improve coverage

This commit is contained in:
booleanmaybe 2026-04-04 00:27:20 -04:00
parent f99a76fa56
commit d78b187ac0
5 changed files with 766 additions and 0 deletions

View file

@ -2,6 +2,7 @@ package runtime
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
@ -419,3 +420,81 @@ func nonEmptyLines(s string) []string {
}
return result
}
func TestTableFormatterWriteError(t *testing.T) {
proj := &ruki.TaskProjection{
Fields: []string{"id"},
Tasks: []*task.Task{{ID: "TIKI-ABC123"}},
}
ew := &errorWriter{failAfter: 0}
err := NewTableFormatter().Format(ew, proj)
if err == nil {
t.Fatal("expected write error")
}
}
type errorWriter struct {
writes int
failAfter int
}
func (w *errorWriter) Write(p []byte) (int, error) {
if w.writes >= w.failAfter {
return 0, fmt.Errorf("write error")
}
w.writes++
return len(p), nil
}
func TestRenderListNilSlice(t *testing.T) {
got := renderList(nil)
if got != "" {
t.Errorf("expected empty for nil, got %q", got)
}
}
func TestRenderListNonStringSlice(t *testing.T) {
got := renderList(42)
if got != "" {
t.Errorf("expected empty for non-slice, got %q", got)
}
}
func TestRenderIntNonInt(t *testing.T) {
got := renderInt("not an int")
if got != "" {
t.Errorf("expected empty for non-int, got %q", got)
}
}
func TestRenderDateNonTime(t *testing.T) {
got := renderDate("not a time")
if got != "" {
t.Errorf("expected empty for non-time, got %q", got)
}
}
func TestRenderTimestampNonTime(t *testing.T) {
got := renderTimestamp("not a time")
if got != "" {
t.Errorf("expected empty for non-time, got %q", got)
}
}
func TestEscapeScalar(t *testing.T) {
tests := []struct {
input, want string
}{
{"hello", "hello"},
{"line\nnewline", `line\nnewline`},
{"tab\there", `tab\there`},
{`back\slash`, `back\\slash`},
}
for _, tt := range tests {
got := escapeScalar(tt.input)
if got != tt.want {
t.Errorf("escapeScalar(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}

View file

@ -140,3 +140,10 @@ func TestMapValueTypeCompleteness(t *testing.T) {
seen[rv] = wt
}
}
func TestMapValueTypeUnknownFallback(t *testing.T) {
got := mapValueType(workflow.ValueType(999))
if got != ruki.ValueString {
t.Errorf("expected fallback to ValueString, got %d", got)
}
}

View file

@ -203,3 +203,19 @@ func TestKeywordInIdentPosition_ParseError(t *testing.T) {
})
}
}
func TestIsReservedKeyword(t *testing.T) {
reserved := []string{"select", "SELECT", "where", "create", "update", "delete", "and", "or", "not", "in", "is", "empty", "any", "all", "set", "order", "by", "asc", "desc", "before", "after", "deny", "run", "old", "new"}
for _, kw := range reserved {
if !IsReservedKeyword(kw) {
t.Errorf("expected %q to be reserved", kw)
}
}
notReserved := []string{"title", "status", "foo", "bar", "hello"}
for _, kw := range notReserved {
if IsReservedKeyword(kw) {
t.Errorf("expected %q to NOT be reserved", kw)
}
}
}

View file

@ -461,6 +461,7 @@ func TestUnquoteString(t *testing.T) {
{"no quotes", "bare", "bare"},
{"empty quotes", `""`, ""},
{"single char", `"x"`, "x"},
{"invalid escape fallback", "\"bad\\qescape\"", "bad\\qescape"},
}
for _, tt := range tests {
@ -472,3 +473,242 @@ func TestUnquoteString(t *testing.T) {
})
}
}
// TestLower_EmptyStatement exercises the default branch of lowerStatement.
func TestLower_EmptyStatement(t *testing.T) {
_, err := lowerStatement(&statementGrammar{})
if err == nil {
t.Fatal("expected error for empty statement grammar")
}
if err.Error() != "empty statement" {
t.Errorf("expected 'empty statement', got: %v", err)
}
}
// TestLower_EmptyTriggerAction exercises the default branch of lowerTriggerAction.
func TestLower_EmptyTriggerAction(t *testing.T) {
trig := &Trigger{Timing: "after", Event: "update"}
err := lowerTriggerAction(&actionGrammar{}, trig)
if err == nil {
t.Fatal("expected error for empty trigger action")
}
if err.Error() != "empty trigger action" {
t.Errorf("expected 'empty trigger action', got: %v", err)
}
}
// TestLower_EmptyExpression exercises the default branch of lowerUnary.
func TestLower_EmptyExpression(t *testing.T) {
_, err := lowerUnary(&unaryExpr{})
if err == nil {
t.Fatal("expected error for empty expression")
}
if err.Error() != "empty expression" {
t.Errorf("expected 'empty expression', got: %v", err)
}
}
// TestLower_InvalidDateLiteral exercises parseDateLiteral error path.
func TestLower_InvalidDateLiteral(t *testing.T) {
_, err := parseDateLiteral("not-a-date")
if err == nil {
t.Fatal("expected error for invalid date literal")
}
}
// TestLower_InvalidDurationLiteral exercises parseDurationLiteral error paths.
func TestLower_InvalidDurationLiteral(t *testing.T) {
tests := []struct {
name string
input string
}{
{"no digits", "days"},
{"no unit", "123"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := parseDurationLiteral(tt.input)
if err == nil {
t.Fatal("expected error for invalid duration")
}
})
}
}
// TestLower_SubQueryOrderByRejected exercises the order by rejection in lowerSubQuery.
func TestLower_SubQueryOrderByRejected(t *testing.T) {
dir := "asc"
sq := &subQueryExpr{
OrderBy: &orderByGrammar{
First: orderByField{Field: "priority", Direction: &dir},
},
}
_, err := lowerSubQuery(sq)
if err == nil {
t.Fatal("expected error for order by in subquery")
}
if err.Error() != "order by is not valid inside a subquery" {
t.Errorf("unexpected error: %v", err)
}
}
// TestLower_ExprCondBareExpression exercises the default branch of lowerExprCond.
func TestLower_ExprCondBareExpression(t *testing.T) {
field := "title"
ec := &exprCond{
Left: exprGrammar{
Left: unaryExpr{FieldRef: &field},
},
}
_, err := lowerExprCond(ec)
if err == nil {
t.Fatal("expected error for bare expression as condition")
}
if err.Error() != "expression used as condition without comparison operator" {
t.Errorf("unexpected error: %v", err)
}
}
// TestLower_TriggerRunAction exercises the run action branch in lowerTriggerAction.
func TestLower_TriggerRunAction(t *testing.T) {
p := newTestParser()
trig, err := p.ParseTrigger(`after update run("echo done")`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if trig.Run == nil {
t.Fatal("expected non-nil run action")
}
}
// TestLower_TriggerUpdateAction exercises the update action branch in lowerTriggerAction.
func TestLower_TriggerUpdateAction(t *testing.T) {
p := newTestParser()
trig, err := p.ParseTrigger(`after update where new.status = "done" update where id = new.id set priority=1`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if trig.Action == nil || trig.Action.Update == nil {
t.Fatal("expected update action in trigger")
}
}
// TestLower_TriggerDeleteAction exercises the delete action branch in lowerTriggerAction.
func TestLower_TriggerDeleteAction(t *testing.T) {
p := newTestParser()
trig, err := p.ParseTrigger(`after update where new.status = "done" delete where id = new.id`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if trig.Action == nil || trig.Action.Delete == nil {
t.Fatal("expected delete action in trigger")
}
}
// TestLower_TriggerCreateAction exercises the create action branch in lowerTriggerAction.
func TestLower_TriggerCreateAction(t *testing.T) {
p := newTestParser()
trig, err := p.ParseTrigger(`after update where new.status = "done" create title="follow-up"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if trig.Action == nil || trig.Action.Create == nil {
t.Fatal("expected create action in trigger")
}
}
// TestLower_TriggerWithoutWhereOrAction exercises trigger lowering with no where and no action.
func TestLower_TriggerWithoutWhereOrAction(t *testing.T) {
p := newTestParser()
trig, err := p.ParseTrigger(`before delete deny "forbidden"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if trig.Where != nil {
t.Error("expected nil where")
}
if trig.Deny == nil {
t.Fatal("expected non-nil deny")
}
}
// TestLower_DeleteStatement exercises the delete lowering path.
func TestLower_DeleteStatement(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`delete where status = "done"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Delete == nil {
t.Fatal("expected delete statement")
}
if stmt.Delete.Where == nil {
t.Fatal("expected non-nil where in delete")
}
}
// TestLower_UpdateStatement exercises the update lowering path.
func TestLower_UpdateStatement(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`update where status = "done" set priority=1`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Update == nil {
t.Fatal("expected update statement")
}
}
// TestLower_CreateStatement exercises the create lowering path.
func TestLower_CreateStatement(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`create title="hello"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Create == nil {
t.Fatal("expected create statement")
}
}
// TestLower_OrCondChain exercises the or condition chaining in lowerOrCond.
func TestLower_OrCondChain(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where status = "done" or status = "ready" or status = "backlog"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
bc, ok := stmt.Select.Where.(*BinaryCondition)
if !ok {
t.Fatalf("expected BinaryCondition, got %T", stmt.Select.Where)
}
if bc.Op != "or" {
t.Errorf("expected 'or', got %q", bc.Op)
}
}
// TestLower_AndCondChain exercises the and condition chaining in lowerAndCond.
func TestLower_AndCondChain(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where priority = 1 and status = "done" and assignee = "bob"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
bc, ok := stmt.Select.Where.(*BinaryCondition)
if !ok {
t.Fatalf("expected BinaryCondition, got %T", stmt.Select.Where)
}
if bc.Op != "and" {
t.Errorf("expected 'and', got %q", bc.Op)
}
}

View file

@ -1671,3 +1671,427 @@ func TestValidation_ListAssignmentRejectsFieldRefs(t *testing.T) {
})
}
}
func TestValidation_TypeNameCoverage(t *testing.T) {
tests := []struct {
typ ValueType
want string
}{
{ValueString, "string"},
{ValueInt, "int"},
{ValueDate, "date"},
{ValueTimestamp, "timestamp"},
{ValueDuration, "duration"},
{ValueBool, "bool"},
{ValueID, "id"},
{ValueRef, "ref"},
{ValueRecurrence, "recurrence"},
{ValueListString, "list<string>"},
{ValueListRef, "list<ref>"},
{ValueStatus, "status"},
{ValueTaskType, "type"},
{-1, "empty"},
{ValueType(999), "unknown"},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
got := typeName(tt.typ)
if got != tt.want {
t.Errorf("typeName(%d) = %q, want %q", tt.typ, got, tt.want)
}
})
}
}
func TestValidation_ResolveEmptyPairBothEmpty(t *testing.T) {
a, b := resolveEmptyPair(-1, -1)
if a != -1 || b != -1 {
t.Errorf("expected both -1, got %d, %d", a, b)
}
}
func TestValidation_MembershipCompatibleEdgeCases(t *testing.T) {
tests := []struct {
name string
a, b ValueType
want bool
}{
{"same type", ValueString, ValueString, true},
{"empty a", -1, ValueString, true},
{"empty b", ValueString, -1, true},
{"id and ref", ValueID, ValueRef, true},
{"ref and id", ValueRef, ValueID, true},
{"string and int", ValueString, ValueInt, false},
{"status and string", ValueStatus, ValueString, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := membershipCompatible(tt.a, tt.b)
if got != tt.want {
t.Errorf("membershipCompatible(%d, %d) = %v, want %v", tt.a, tt.b, got, tt.want)
}
})
}
}
func TestValidation_IsRefCompatible(t *testing.T) {
tests := []struct {
typ ValueType
want bool
}{
{ValueRef, true},
{ValueID, true},
{ValueString, false},
{ValueInt, false},
{ValueListRef, false},
}
for _, tt := range tests {
t.Run(typeName(tt.typ), func(t *testing.T) {
got := isRefCompatible(tt.typ)
if got != tt.want {
t.Errorf("isRefCompatible(%d) = %v, want %v", tt.typ, got, tt.want)
}
})
}
}
func TestValidation_CheckCompareOpUnknown(t *testing.T) {
err := checkCompareOp(ValueInt, "~")
if err == nil {
t.Fatal("expected error for unknown operator")
}
if !strings.Contains(err.Error(), "unknown operator") {
t.Errorf("expected 'unknown operator', got: %v", err)
}
}
func TestValidation_IsOrderableType(t *testing.T) {
orderable := []ValueType{ValueInt, ValueDate, ValueTimestamp, ValueDuration, ValueString, ValueStatus, ValueTaskType, ValueID, ValueRef}
for _, vt := range orderable {
if !isOrderableType(vt) {
t.Errorf("expected %s to be orderable", typeName(vt))
}
}
notOrderable := []ValueType{ValueBool, ValueListString, ValueListRef, ValueRecurrence}
for _, vt := range notOrderable {
if isOrderableType(vt) {
t.Errorf("expected %s to NOT be orderable", typeName(vt))
}
}
}
func TestValidation_IsStringLike(t *testing.T) {
stringLike := []ValueType{ValueString, ValueStatus, ValueTaskType, ValueID, ValueRef}
for _, vt := range stringLike {
if !isStringLike(vt) {
t.Errorf("expected %s to be string-like", typeName(vt))
}
}
notStringLike := []ValueType{ValueInt, ValueDate, ValueBool, ValueListString}
for _, vt := range notStringLike {
if isStringLike(vt) {
t.Errorf("expected %s to NOT be string-like", typeName(vt))
}
}
}
func TestValidation_IsEnumType(t *testing.T) {
if !isEnumType(ValueStatus) {
t.Error("expected ValueStatus to be enum")
}
if !isEnumType(ValueTaskType) {
t.Error("expected ValueTaskType to be enum")
}
if isEnumType(ValueString) {
t.Error("expected ValueString to NOT be enum")
}
}
func TestValidation_TypesCompatible(t *testing.T) {
tests := []struct {
name string
a, b ValueType
want bool
}{
{"same type", ValueInt, ValueInt, true},
{"empty a", -1, ValueString, true},
{"empty b", ValueString, -1, true},
{"string-like pair", ValueString, ValueID, true},
{"status and string", ValueStatus, ValueString, true},
{"int and string", ValueInt, ValueString, false},
{"date and int", ValueDate, ValueInt, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := typesCompatible(tt.a, tt.b)
if got != tt.want {
t.Errorf("typesCompatible(%s, %s) = %v, want %v", typeName(tt.a), typeName(tt.b), got, tt.want)
}
})
}
}
func TestValidation_InferBinaryExprUnknownOp(t *testing.T) {
p := newTestParser()
b := &BinaryExpr{
Op: "*",
Left: &IntLiteral{Value: 1},
Right: &IntLiteral{Value: 2},
}
_, err := p.inferBinaryExprType(b)
if err == nil {
t.Fatal("expected error for unknown binary operator")
}
if !strings.Contains(err.Error(), "unknown binary operator") {
t.Errorf("expected 'unknown binary operator', got: %v", err)
}
}
func TestValidation_InferExprTypeSubqueryBare(t *testing.T) {
p := newTestParser()
sq := &SubQuery{}
_, err := p.inferExprType(sq)
if err == nil {
t.Fatal("expected error for bare subquery")
}
if !strings.Contains(err.Error(), "subquery is only valid as argument to count") {
t.Errorf("unexpected error: %v", err)
}
}
type valFakeCondition struct{}
func (*valFakeCondition) conditionNode() {}
func TestValidation_ValidateConditionUnknownType(t *testing.T) {
p := newTestParser()
err := p.validateCondition(&valFakeCondition{})
if err == nil {
t.Fatal("expected error for unknown condition type")
}
if !strings.Contains(err.Error(), "unknown condition type") {
t.Errorf("unexpected error: %v", err)
}
}
type valFakeExpr struct{}
func (*valFakeExpr) exprNode() {}
func TestValidation_InferExprTypeUnknownType(t *testing.T) {
p := newTestParser()
_, err := p.inferExprType(&valFakeExpr{})
if err == nil {
t.Fatal("expected error for unknown expression type")
}
if !strings.Contains(err.Error(), "unknown expression type") {
t.Errorf("unexpected error: %v", err)
}
}
func TestValidation_ListElementType(t *testing.T) {
tests := []struct {
typ ValueType
want ValueType
}{
{ValueListString, ValueString},
{ValueListRef, ValueRef},
{ValueString, -1},
{ValueInt, -1},
}
for _, tt := range tests {
got := listElementType(tt.typ)
if got != tt.want {
t.Errorf("listElementType(%s) = %d, want %d", typeName(tt.typ), got, tt.want)
}
}
}
func TestValidation_BeforeTriggerValidation(t *testing.T) {
p := newTestParser()
tests := []struct {
name string
input string
wantErr string
}{
{
"before with action",
`before update update where id = "x" set status="done"`,
"before-trigger must not have an action",
},
{
"before without deny",
`before update where status = "done"`,
"before-trigger must have deny",
},
{
"after with deny",
`after update deny "no"`,
"after-trigger must not have deny",
},
{
"after without action",
`after update where status = "done"`,
"after-trigger must have an action",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := p.ParseTrigger(tt.input)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got: %v", tt.wantErr, err)
}
})
}
}
func TestValidation_InferListTypeRefElements(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`create title="x" dependsOn=["TIKI-A", "TIKI-B"]`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Create == nil {
t.Fatal("expected create statement")
}
}
func TestValidation_InferListTypeEmptyList(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`create title="x" tags=[]`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Create == nil {
t.Fatal("expected create statement")
}
}
func TestValidation_DateMinusDuration(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`select where due > 2026-03-25 - 1week`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidation_ListRefPlusIdField(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`create title="x" dependsOn=dependsOn + id`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidation_ListRefMinusIdField(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`create title="x" dependsOn=dependsOn - id`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidation_ListRefMinusListRef(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`create title="x" dependsOn=dependsOn - dependsOn`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidation_IntMinusInt(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`create title="x" priority=3 - 1`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidation_ListStringPlusListString(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`create title="x" tags=tags + tags`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidation_ListStringMinusListString(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`create title="x" tags=tags - tags`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidation_ListRefPlusListRef(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`create title="x" dependsOn=dependsOn + dependsOn`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidation_FuncArgRangeError(t *testing.T) {
p := newTestParser()
fc := &FunctionCall{
Name: "contains",
Args: []Expr{&StringLiteral{Value: "a"}},
}
_, err := p.inferFuncCallType(fc)
if err == nil {
t.Fatal("expected error for wrong arg count")
}
if !strings.Contains(err.Error(), "expects 2 argument(s)") {
t.Errorf("unexpected error: %v", err)
}
}
func TestValidation_CheckCompareCompatBothEnums(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`select where status = type`)
if err == nil {
t.Fatal("expected error comparing status with type")
}
if !strings.Contains(err.Error(), "cannot compare") {
t.Errorf("unexpected error: %v", err)
}
}
func TestValidation_ListRefAssignFromListString(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`create title="x" dependsOn=tags`)
if err == nil {
t.Fatal("expected error assigning list<string> to list<ref>")
}
if !strings.Contains(err.Error(), "cannot assign") {
t.Errorf("unexpected error: %v", err)
}
}