Skip to content

Commit d8c8eee

Browse files
akoclaude
andcommitted
test: add unit tests for QUAL005 missing translations linter rule
6 tests covering: - Complete translations (no violations) - Missing language detection (nl_NL missing for one element) - Single-language project (no violations — nothing to compare) - Non-translatable strings ignored (empty language) - Three languages with multiple missing translations - Rule metadata (ID, category, severity) Also add NewLintContextFromDB() helper to create LintContext from a raw sql.DB for testing with in-memory SQLite databases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a060152 commit d8c8eee

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

mdl/linter/context.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ func NewLintContext(cat *catalog.Catalog) *LintContext {
3737
}
3838
}
3939

40+
// NewLintContextFromDB creates a new LintContext from a raw database connection.
41+
// Used in tests to provide an in-memory database with test data.
42+
func NewLintContextFromDB(db *sql.DB) *LintContext {
43+
return &LintContext{
44+
db: db,
45+
excluded: make(map[string]bool),
46+
}
47+
}
48+
4049
// SetExcludedModules sets the list of modules to exclude from linting.
4150
func (ctx *LintContext) SetExcludedModules(modules []string) {
4251
ctx.excluded = make(map[string]bool)
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package rules
4+
5+
import (
6+
"database/sql"
7+
"testing"
8+
9+
"github.com/mendixlabs/mxcli/mdl/linter"
10+
11+
_ "modernc.org/sqlite"
12+
)
13+
14+
// setupTranslationsDB creates an in-memory SQLite database with the strings FTS5 table
15+
// and inserts test data.
16+
func setupTranslationsDB(t *testing.T, rows [][]string) *sql.DB {
17+
t.Helper()
18+
db, err := sql.Open("sqlite", ":memory:")
19+
if err != nil {
20+
t.Fatalf("failed to open in-memory db: %v", err)
21+
}
22+
23+
_, err = db.Exec(`CREATE VIRTUAL TABLE strings USING fts5(
24+
QualifiedName, ObjectType, StringValue, StringContext, Language, ModuleName
25+
)`)
26+
if err != nil {
27+
t.Fatalf("failed to create strings table: %v", err)
28+
}
29+
30+
stmt, err := db.Prepare(`INSERT INTO strings (QualifiedName, ObjectType, StringValue, StringContext, Language, ModuleName)
31+
VALUES (?, ?, ?, ?, ?, ?)`)
32+
if err != nil {
33+
t.Fatalf("failed to prepare insert: %v", err)
34+
}
35+
defer stmt.Close()
36+
37+
for _, row := range rows {
38+
if len(row) != 6 {
39+
t.Fatalf("expected 6 columns, got %d", len(row))
40+
}
41+
_, err := stmt.Exec(row[0], row[1], row[2], row[3], row[4], row[5])
42+
if err != nil {
43+
t.Fatalf("failed to insert row: %v", err)
44+
}
45+
}
46+
47+
return db
48+
}
49+
50+
func TestMissingTranslations_NoViolationsWhenComplete(t *testing.T) {
51+
db := setupTranslationsDB(t, [][]string{
52+
{"MyModule.HomePage", "PAGE", "Welcome", "page_title", "en_US", "MyModule"},
53+
{"MyModule.HomePage", "PAGE", "Welkom", "page_title", "nl_NL", "MyModule"},
54+
})
55+
defer db.Close()
56+
57+
ctx := linter.NewLintContextFromDB(db)
58+
rule := NewMissingTranslationsRule()
59+
violations := rule.Check(ctx)
60+
61+
if len(violations) != 0 {
62+
t.Errorf("expected 0 violations, got %d: %v", len(violations), violations)
63+
}
64+
}
65+
66+
func TestMissingTranslations_DetectsMissingLanguage(t *testing.T) {
67+
db := setupTranslationsDB(t, [][]string{
68+
{"MyModule.HomePage", "PAGE", "Welcome", "page_title", "en_US", "MyModule"},
69+
{"MyModule.HomePage", "PAGE", "Welkom", "page_title", "nl_NL", "MyModule"},
70+
{"MyModule.EditCustomer", "PAGE", "Edit Customer", "page_title", "en_US", "MyModule"},
71+
// nl_NL translation missing for EditCustomer
72+
})
73+
defer db.Close()
74+
75+
ctx := linter.NewLintContextFromDB(db)
76+
rule := NewMissingTranslationsRule()
77+
violations := rule.Check(ctx)
78+
79+
if len(violations) != 1 {
80+
t.Fatalf("expected 1 violation, got %d: %v", len(violations), violations)
81+
}
82+
83+
v := violations[0]
84+
if v.RuleID != "QUAL005" {
85+
t.Errorf("expected rule ID QUAL005, got %s", v.RuleID)
86+
}
87+
if v.Location.DocumentName != "MyModule.EditCustomer" {
88+
t.Errorf("expected document MyModule.EditCustomer, got %s", v.Location.DocumentName)
89+
}
90+
}
91+
92+
func TestMissingTranslations_SingleLanguageNoViolations(t *testing.T) {
93+
// Only one language in the project — nothing to compare
94+
db := setupTranslationsDB(t, [][]string{
95+
{"MyModule.HomePage", "PAGE", "Welcome", "page_title", "en_US", "MyModule"},
96+
{"MyModule.EditCustomer", "PAGE", "Edit Customer", "page_title", "en_US", "MyModule"},
97+
})
98+
defer db.Close()
99+
100+
ctx := linter.NewLintContextFromDB(db)
101+
rule := NewMissingTranslationsRule()
102+
violations := rule.Check(ctx)
103+
104+
if len(violations) != 0 {
105+
t.Errorf("expected 0 violations for single-language project, got %d", len(violations))
106+
}
107+
}
108+
109+
func TestMissingTranslations_NonTranslatableStringsIgnored(t *testing.T) {
110+
// Non-translatable strings (empty language) should not trigger violations
111+
db := setupTranslationsDB(t, [][]string{
112+
{"MyModule.HomePage", "PAGE", "Welcome", "page_title", "en_US", "MyModule"},
113+
{"MyModule.HomePage", "PAGE", "Welkom", "page_title", "nl_NL", "MyModule"},
114+
{"MyModule.HomePage", "PAGE", "/home", "page_url", "", "MyModule"},
115+
{"MyModule.ACT_Process", "MICROFLOW", "Processing items", "documentation", "", "MyModule"},
116+
})
117+
defer db.Close()
118+
119+
ctx := linter.NewLintContextFromDB(db)
120+
rule := NewMissingTranslationsRule()
121+
violations := rule.Check(ctx)
122+
123+
if len(violations) != 0 {
124+
t.Errorf("expected 0 violations, got %d: %v", len(violations), violations)
125+
}
126+
}
127+
128+
func TestMissingTranslations_ThreeLanguages(t *testing.T) {
129+
db := setupTranslationsDB(t, [][]string{
130+
{"MyModule.HomePage", "PAGE", "Welcome", "page_title", "en_US", "MyModule"},
131+
{"MyModule.HomePage", "PAGE", "Welkom", "page_title", "nl_NL", "MyModule"},
132+
{"MyModule.HomePage", "PAGE", "Bienvenue", "page_title", "fr_FR", "MyModule"},
133+
// EditCustomer only has en_US — missing nl_NL and fr_FR
134+
{"MyModule.EditCustomer", "PAGE", "Edit Customer", "page_title", "en_US", "MyModule"},
135+
})
136+
defer db.Close()
137+
138+
ctx := linter.NewLintContextFromDB(db)
139+
rule := NewMissingTranslationsRule()
140+
violations := rule.Check(ctx)
141+
142+
if len(violations) != 2 {
143+
t.Errorf("expected 2 violations (nl_NL + fr_FR missing), got %d: %v", len(violations), violations)
144+
}
145+
}
146+
147+
func TestMissingTranslationsRuleMetadata(t *testing.T) {
148+
rule := NewMissingTranslationsRule()
149+
150+
if rule.ID() != "QUAL005" {
151+
t.Errorf("expected ID QUAL005, got %s", rule.ID())
152+
}
153+
if rule.Category() != "quality" {
154+
t.Errorf("expected category 'quality', got %s", rule.Category())
155+
}
156+
if rule.DefaultSeverity() != linter.SeverityWarning {
157+
t.Errorf("expected severity Warning, got %v", rule.DefaultSeverity())
158+
}
159+
}

0 commit comments

Comments
 (0)