-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrule_test.go
More file actions
348 lines (301 loc) · 10.7 KB
/
Copy pathrule_test.go
File metadata and controls
348 lines (301 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
package concisenessscoring
import (
"errors"
"sync"
"testing"
"github.com/jeduden/mdsmith/internal/lint"
"github.com/jeduden/mdsmith/internal/rules/concisenessscoring/classifier"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func modelConciseness(t *testing.T) float64 {
t.Helper()
m, err := classifier.LoadEmbedded()
require.NoError(t, err)
return 1.0 - m.Threshold()
}
func verboseParagraph() string {
return "Basically, it seems that we are just trying to explain the " +
"same idea in order to make it very clear, and it appears that " +
"we are really saying very little new information overall."
}
func conciseParagraph() string {
return "The release process validates changelog links, updates " +
"version tags, and publishes checksums so reviewers can verify " +
"artifacts before promoting a build."
}
func TestCheck_LowScore(t *testing.T) {
src := []byte(verboseParagraph() + "\n")
f, err := lint.NewFile("test.md", src)
require.NoError(t, err)
threshold := modelConciseness(t)
r := &Rule{
MinScore: threshold,
MinWords: 20,
}
diags := r.Check(f)
require.Len(t, diags, 1, "expected 1 diagnostic, got %d", len(diags))
d := diags[0]
assert.Equal(t, "MDS029", d.RuleID)
assert.Equal(t, "conciseness-scoring", d.RuleName)
assert.Equal(t, lint.Warning, d.Severity)
assert.Contains(t, d.Message, "conciseness score too low")
assert.Contains(t, d.Message, "target >=")
}
func TestCheck_HighScore(t *testing.T) {
src := []byte(conciseParagraph() + "\n")
f, err := lint.NewFile("test.md", src)
require.NoError(t, err)
r := &Rule{
MinScore: 0.10,
MinWords: 20,
}
diags := r.Check(f)
require.Len(t, diags, 0, "expected 0 diagnostics, got %d", len(diags))
}
func TestCheck_ShortParagraphSkipped(t *testing.T) {
src := []byte("This is short and intentionally skipped.\n")
f, err := lint.NewFile("test.md", src)
require.NoError(t, err)
r := &Rule{
MinScore: 0.95,
MinWords: 20,
}
diags := r.Check(f)
require.Len(t, diags, 0, "expected 0 diagnostics, got %d", len(diags))
}
func TestCheck_DiagnosticLine(t *testing.T) {
src := []byte("# Heading\n\n" + verboseParagraph() + "\n")
f, err := lint.NewFile("test.md", src)
require.NoError(t, err)
threshold := modelConciseness(t)
r := &Rule{
MinScore: threshold,
MinWords: 20,
}
diags := r.Check(f)
require.Len(t, diags, 1, "expected 1 diagnostic, got %d", len(diags))
if diags[0].Line != 3 {
t.Errorf("expected line 3, got %d", diags[0].Line)
}
}
func TestCheck_TableSkipped(t *testing.T) {
src := []byte(
"| Setting | Value |\n" +
"|---------|-------|\n" +
"| alpha | beta |\n",
)
f, err := lint.NewFile("test.md", src)
require.NoError(t, err)
r := &Rule{
MinScore: 0.95,
MinWords: 1,
}
diags := r.Check(f)
require.Len(t, diags, 0, "expected 0 diagnostics for table, got %d", len(diags))
}
func TestApplySettings_Valid(t *testing.T) {
r := &Rule{MinScore: defaultMinScore, MinWords: defaultMinWords}
err := r.ApplySettings(map[string]any{
"min-score": 0.5,
"min-words": 30,
})
require.NoError(t, err, "unexpected error: %v", err)
if r.MinScore != 0.5 {
t.Errorf("expected MinScore=0.5, got %.2f", r.MinScore)
}
if r.MinWords != 30 {
t.Errorf("expected MinWords=30, got %d", r.MinWords)
}
}
func TestApplySettings_RemovedListSettings(t *testing.T) {
r := &Rule{MinScore: defaultMinScore, MinWords: defaultMinWords}
err := r.ApplySettings(map[string]any{"filler-words": []any{"test"}})
require.Error(t, err, "filler-words should be unknown after removal")
}
func TestApplySettings_InvalidMinScoreType(t *testing.T) {
r := &Rule{}
err := r.ApplySettings(map[string]any{"min-score": "high"})
require.Error(t, err, "expected error for non-number min-score")
}
func TestApplySettings_InvalidMinScoreRange(t *testing.T) {
r := &Rule{}
err := r.ApplySettings(map[string]any{"min-score": 1.2})
require.Error(t, err, "expected error for out-of-range min-score")
}
func TestApplySettings_UnknownKey(t *testing.T) {
r := &Rule{}
err := r.ApplySettings(map[string]any{"unknown": true})
require.Error(t, err, "expected error for unknown setting")
}
func TestDefaultSettings(t *testing.T) {
r := &Rule{}
ds := r.DefaultSettings()
if ds["min-score"] != defaultMinScore {
t.Errorf(
"expected min-score=%.2f, got %v",
defaultMinScore, ds["min-score"],
)
}
if ds["min-words"] != defaultMinWords {
t.Errorf(
"expected min-words=%d, got %v",
defaultMinWords, ds["min-words"],
)
}
}
func TestID(t *testing.T) {
r := &Rule{}
if r.ID() != "MDS029" {
t.Errorf("expected MDS029, got %s", r.ID())
}
}
func TestName(t *testing.T) {
r := &Rule{}
if r.Name() != "conciseness-scoring" {
t.Errorf("expected conciseness-scoring, got %s", r.Name())
}
}
func TestCategory(t *testing.T) {
r := &Rule{}
if r.Category() != "prose" {
t.Errorf("expected meta, got %s", r.Category())
}
}
func TestEnabledByDefault(t *testing.T) {
r := &Rule{}
assert.False(t, r.EnabledByDefault(), "conciseness-scoring should be disabled by default")
}
// --- formatExamples branch coverage ---
// TestFormatExamples_Empty exercises the len==0 branch of formatExamples.
func TestFormatExamples_Empty(t *testing.T) {
result := formatExamples([]string{})
assert.Equal(t, "", result)
}
// TestFormatExamples_SingleExample exercises the `len < limit` branch of
// formatExamples, which caps at min(2, len).
func TestFormatExamples_SingleExample(t *testing.T) {
result := formatExamples([]string{"basically"})
assert.Contains(t, result, "basically")
// Only one example, so no comma separator.
assert.NotContains(t, result, ", ")
}
// --- setMinWords error branches ---
// TestApplySettings_InvalidMinWordsType exercises the non-int path in setMinWords.
func TestApplySettings_InvalidMinWordsType(t *testing.T) {
r := &Rule{}
err := r.ApplySettings(map[string]any{"min-words": "twenty"})
require.Error(t, err, "expected error for non-integer min-words")
assert.Contains(t, err.Error(), "min-words must be an integer")
}
// TestApplySettings_MinWordsZero exercises the n<=0 path in setMinWords.
func TestApplySettings_MinWordsZero(t *testing.T) {
r := &Rule{}
err := r.ApplySettings(map[string]any{"min-words": 0})
require.Error(t, err, "expected error for min-words=0")
assert.Contains(t, err.Error(), "min-words must be > 0")
}
// TestApplySettings_MinWordsNegative exercises the n<=0 path in setMinWords
// with a negative value.
func TestApplySettings_MinWordsNegative(t *testing.T) {
r := &Rule{}
err := r.ApplySettings(map[string]any{"min-words": -5})
require.Error(t, err, "expected error for negative min-words")
assert.Contains(t, err.Error(), "min-words must be > 0")
}
// TestApplySettings_MinWordsValid exercises the success path when setMinWords
// is called via ApplySettings so that the err!=nil return is also covered.
func TestApplySettings_MinWordsValid(t *testing.T) {
r := &Rule{MinScore: defaultMinScore, MinWords: defaultMinWords}
err := r.ApplySettings(map[string]any{"min-words": 10})
require.NoError(t, err)
assert.Equal(t, 10, r.MinWords)
}
// =====================================================================
// Phase 5: additional branch coverage
// =====================================================================
// TestCheck_LoadError exercises the loadErrorDiag path by injecting a
// scorer load error via package-level state reset.
func TestCheck_LoadError(t *testing.T) {
// Save non-Once state and restore after the test.
origScorer := globalScorer
origErr := scorerErr
t.Cleanup(func() {
// Reset Once objects (cannot copy sync.Once) and restore scorer state.
// Leave scorerOnce unconsumed so later loadScorer() calls can safely
// re-initialize instead of observing a done Once with stale nil state.
scorerOnce = sync.Once{}
errReportedOnce = sync.Once{}
globalScorer = origScorer
scorerErr = origErr
})
// Inject a fake error so loadScorer() returns it.
scorerOnce = sync.Once{}
errReportedOnce = sync.Once{}
globalScorer = nil
scorerErr = errors.New("injected classifier load failure")
// Pre-consume the once so loadScorer returns the error immediately.
scorerOnce.Do(func() {}) // no-op; scorerErr is already set
src := []byte("# Title\n")
f, err := lint.NewFile("test.md", src)
require.NoError(t, err)
r := &Rule{MinScore: defaultMinScore, MinWords: defaultMinWords}
diags := r.Check(f)
require.Len(t, diags, 1, "expected 1 error diagnostic")
assert.Equal(t, lint.Error, diags[0].Severity)
assert.Contains(t, diags[0].Message, "classifier load failed")
// Second call: errReportedOnce suppresses the error.
diags2 := r.Check(f)
assert.Empty(t, diags2, "second call should not repeat the error diagnostic")
}
// TestNewScorer_Success verifies NewScorer succeeds with the embedded artifact.
func TestNewScorer_Success(t *testing.T) {
s, err := NewScorer()
require.NoError(t, err)
assert.NotNil(t, s)
}
func TestCheck_MessageNoConcatenationWhenExamplesPresent(t *testing.T) {
// When verbose cues are found, the diagnostic message must include
// the cue examples in a single fmt.Sprintf rather than via `message +=`
// concatenation. This test verifies the combined message format so the
// implementation cannot silently drop the cue text.
src := []byte(verboseParagraph() + "\n")
f, err := lint.NewFile("test.md", src)
require.NoError(t, err)
threshold := modelConciseness(t)
r := &Rule{MinScore: threshold, MinWords: 20}
diags := r.Check(f)
if len(diags) == 0 {
t.Skip("model threshold did not trigger on fixture")
}
msg := diags[0].Message
// The message must contain both the score summary and the cue guidance.
assert.Contains(t, msg, "conciseness score too low")
assert.Contains(t, msg, "target >=")
// verboseParagraph always triggers cue detection; both must be present.
assert.Contains(t, msg, "reduce verbose cues")
assert.Contains(t, msg, "e.g.,", "message with cues must include formatted examples")
}
func TestCheck_NoCuesMessage(t *testing.T) {
// Exercises the if examples == "" branch: a paragraph that scores as
// verbose but produces no cue phrases yields the base message only.
noCuePara := "When the configuration is set, the setting is used by the " +
"configuration. The configuration uses the setting and the setting " +
"configures the configuration."
s, err := NewScorer()
require.NoError(t, err)
scored := s.Score(noCuePara)
if len(scored.Cues) > 0 {
t.Skip("scorer detected cues in fixture; model may have drifted")
}
src := []byte(noCuePara + "\n")
f, err := lint.NewFile("test.md", src)
require.NoError(t, err)
r := &Rule{MinScore: scored.Conciseness + 0.10, MinWords: 1}
diags := r.Check(f)
require.Len(t, diags, 1, "expected diagnostic for low-scoring no-cue paragraph")
msg := diags[0].Message
assert.Contains(t, msg, "conciseness score too low")
assert.Contains(t, msg, "target >=")
assert.NotContains(t, msg, "reduce verbose cues", "no cues detected, so no cue guidance")
}