Skip to content

Commit db42a7f

Browse files
committed
fix(files): Ignore included:false pattern
Define specificity of patterns. Let most specific pattern decide to include files or not. Closes #1530
1 parent 8ef475f commit db42a7f

File tree

8 files changed

+338
-28
lines changed

8 files changed

+338
-28
lines changed

docs/config/02-files.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ Each pattern is either a simple string or an object with four properties:
3535
* **Description.** Should the files be included in the browser using
3636
`<script>` tag? Use `false` if you want to load them manually, eg.
3737
using [Require.js](../plus/requirejs.html).
38+
39+
If a file is covered by multiple patterns with different `include` properties, the most specific pattern takes
40+
precedence over the other.
41+
42+
The specificity of the pattern is defined as a six-tuple, where larger tuple implies lesser specificity:
43+
*(n<sub>glob parts</sub>, n<sub>glob star</sub>, n<sub>star</sub>, n<sub>ext glob</sub>, n<sub>range</sub>, n<sub>optional</sub>)*.
44+
Tuples are compared lexicographically.
45+
46+
The *n<sub>glob parts</sub>* is the number of patterns after the bracket sections are expanded. E.g. the
47+
the pattern *{0...9}* will yield *n<sub>glob parts</sub>=10*. The rest of the tuple is decided as the least
48+
specific of each expanded pattern.
3849

3950
### `served`
4051
* **Type.** Boolean

lib/config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ var Pattern = function (pattern, served, included, watched, nocache) {
2828
this.included = helper.isDefined(included) ? included : true
2929
this.watched = helper.isDefined(watched) ? watched : true
3030
this.nocache = helper.isDefined(nocache) ? nocache : false
31+
this.weight = helper.mmPatternWeight(pattern)
32+
}
33+
34+
Pattern.prototype.compare = function (other) {
35+
return helper.mmComparePatternWeights(this.weight, other.weight)
3136
}
3237

3338
var UrlPattern = function (url) {

lib/file-list.js

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -214,28 +214,38 @@ List.prototype._refresh = function () {
214214
Object.defineProperty(List.prototype, 'files', {
215215
get: function () {
216216
var self = this
217+
var uniqueFlat = function (list) {
218+
return _.uniq(_.flatten(list), 'path')
219+
}
217220

218-
var served = this._patterns.filter(function (pattern) {
219-
return pattern.served
220-
})
221-
.map(function (p) {
221+
var expandPattern = function (p) {
222222
return from(self.buckets.get(p.pattern) || []).sort(byPath)
223-
})
223+
}
224224

225-
var included = this._patterns.filter(function (pattern) {
226-
return pattern.included
225+
var served = this._patterns.filter(function (pattern) {
226+
return pattern.served
227227
})
228-
.map(function (p) {
229-
return from(self.buckets.get(p.pattern) || []).sort(byPath)
228+
.map(expandPattern)
229+
230+
var lookup = {}
231+
var included = {}
232+
this._patterns.forEach(function (p) {
233+
var bucket = expandPattern(p)
234+
bucket.forEach(function (file) {
235+
var other = lookup[file.path]
236+
if (other && other.compare(p) < 0) return
237+
lookup[file.path] = p
238+
if (p.included) {
239+
included[file.path] = file
240+
} else {
241+
delete included[file.path]
242+
}
243+
})
230244
})
231245

232-
var uniqFlat = function (list) {
233-
return _.uniq(_.flatten(list), 'path')
234-
}
235-
236246
return {
237-
served: uniqFlat(served),
238-
included: uniqFlat(included)
247+
served: uniqueFlat(served),
248+
included: _.values(included)
239249
}
240250
}
241251
})

lib/helper.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ var path = require('path')
33
var _ = require('lodash')
44
var useragent = require('useragent')
55
var Promise = require('bluebird')
6+
var mm = require('minimatch')
67

78
exports.browserFullNameToShort = function (fullName) {
89
var agent = useragent.parse(fullName)
@@ -13,6 +14,67 @@ exports.browserFullNameToShort = function (fullName) {
1314
exports.isDefined = function (value) {
1415
return !_.isUndefined(value)
1516
}
17+
var parser = function (pattern, out) {
18+
if (pattern.length === 0) return out
19+
var p = /^(\[[^\]]*\]|[\*\+@\?]\((.+?)\))/g
20+
var matches = p.exec(pattern)
21+
if (!matches) {
22+
var c = pattern[0]
23+
var t = 'word'
24+
if (c === '*') {
25+
t = 'star'
26+
} else if (c === '?') {
27+
t = 'optional'
28+
}
29+
out[t]++
30+
return parser(pattern.substring(1), out)
31+
}
32+
if (matches[2] !== undefined) {
33+
out.ext_glob++
34+
parser(matches[2], out)
35+
return parser(pattern.substring(matches[0].length), out)
36+
}
37+
out.range++
38+
return parser(pattern.substring(matches[0].length), out)
39+
}
40+
41+
var gs_parser = function (pattern, out) {
42+
if (pattern === '**') {
43+
out.glob_star++
44+
return out
45+
}
46+
return parser(pattern, out)
47+
}
48+
49+
var compare_weight_object = function (w1, w2) {
50+
return exports.mmComparePatternWeights(
51+
[w1.glob_star, w1.star, w1.ext_glob, w1.range, w1.optional],
52+
[w2.glob_star, w2.star, w2.ext_glob, w2.range, w2.optional]
53+
)
54+
}
55+
56+
exports.mmPatternWeight = function (pattern) {
57+
var m = new mm.Minimatch(pattern)
58+
if (!m.globParts) return [0, 0, 0, 0, 0, 0]
59+
var result = m.globParts.reduce(function (prev, p) {
60+
var r = p.reduce(function (prev, p) {
61+
return gs_parser(p, prev)
62+
}, {glob_star: 0, ext_glob: 0, word: 0, star: 0, optional: 0, range: 0})
63+
if (prev === undefined) return r
64+
return compare_weight_object(r, prev) > 0 ? r : prev
65+
}, undefined)
66+
result.glob_sets = m.set.length
67+
return [result.glob_sets, result.glob_star, result.star, result.ext_glob, result.range, result.optional]
68+
}
69+
70+
exports.mmComparePatternWeights = function (weight1, weight2) {
71+
var n1, n2, diff
72+
n1 = weight1[0]
73+
n2 = weight2[0]
74+
diff = n1 - n2
75+
if (diff !== 0) return diff / Math.abs(diff)
76+
return weight1.length > 1 ? exports.mmComparePatternWeights(weight1.slice(1), weight2.slice(1)) : 0
77+
}
1678

1779
exports.isFunction = _.isFunction
1880
exports.isString = _.isString

test/e2e/files.feature

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
Feature: Including files
2+
In order to use Karma
3+
As a person who wants to write great tests
4+
I want to be able to include and exclude files
5+
6+
Scenario: Execute a test excluding a file
7+
Given a configuration with:
8+
"""
9+
files = [
10+
{pattern: 'files/log_foo.js', included: false},
11+
'files/*.js'
12+
];
13+
browsers = ['PhantomJS'];
14+
plugins = [
15+
'karma-jasmine',
16+
'karma-phantomjs-launcher'
17+
];
18+
"""
19+
When I start Karma
20+
Then it passes with:
21+
"""
22+
.
23+
PhantomJS
24+
"""
25+
26+
Scenario: Execute a test excluding an explicitly included file
27+
Given a configuration with:
28+
"""
29+
files = [
30+
{pattern: 'files/log_foo.js', included: true},
31+
{pattern: 'files/log_foo.js', included: false},
32+
'files/*.js'
33+
];
34+
browsers = ['PhantomJS'];
35+
plugins = [
36+
'karma-jasmine',
37+
'karma-phantomjs-launcher'
38+
];
39+
"""
40+
When I start Karma
41+
Then it passes with:
42+
"""
43+
.
44+
PhantomJS
45+
"""
46+
47+
Scenario: Execute a test excluding an explicitly included file in another order
48+
Given a configuration with:
49+
"""
50+
files = [
51+
'files/*.js',
52+
{pattern: 'files/log_foo.js', included: true},
53+
{pattern: 'files/log_foo.js', included: false}
54+
];
55+
browsers = ['PhantomJS'];
56+
plugins = [
57+
'karma-jasmine',
58+
'karma-phantomjs-launcher'
59+
];
60+
"""
61+
When I start Karma
62+
Then it passes with:
63+
"""
64+
.
65+
PhantomJS
66+
"""
67+
68+
Scenario: Execute a test excluding an file included with brackets patterns
69+
Given a configuration with:
70+
"""
71+
files = [
72+
'files/test.js',
73+
{pattern: 'files/log_foo.js', included: false},
74+
{pattern: 'files/{log,bug}_foo.js', included: true}
75+
];
76+
browsers = ['PhantomJS'];
77+
plugins = [
78+
'karma-jasmine',
79+
'karma-phantomjs-launcher'
80+
];
81+
"""
82+
When I start Karma
83+
Then it passes with:
84+
"""
85+
.
86+
PhantomJS
87+
"""
88+
89+
Scenario: Execute a test excluding an file included with wildcard
90+
Given a configuration with:
91+
"""
92+
files = [
93+
'files/test.js',
94+
{pattern: 'files/+(log|bug)_foo.js', included: false},
95+
{pattern: 'files/*.js', included: true}
96+
];
97+
browsers = ['PhantomJS'];
98+
plugins = [
99+
'karma-jasmine',
100+
'karma-phantomjs-launcher'
101+
];
102+
"""
103+
When I start Karma
104+
Then it passes with:
105+
"""
106+
.
107+
PhantomJS
108+
"""
109+
110+
Scenario: Execute a test excluding an file included with glob-star
111+
Given a configuration with:
112+
"""
113+
files = [
114+
'files/test.js',
115+
{pattern: 'files/*.js', included: false},
116+
{pattern: 'files/**', included: true}
117+
];
118+
browsers = ['PhantomJS'];
119+
plugins = [
120+
'karma-jasmine',
121+
'karma-phantomjs-launcher'
122+
];
123+
"""
124+
When I start Karma
125+
Then it passes with:
126+
"""
127+
.
128+
PhantomJS
129+
"""
130+
131+
132+
Scenario: Execute a test excluding an file included with ext. glob patterns
133+
Given a configuration with:
134+
"""
135+
files = [
136+
'files/test.js',
137+
{pattern: 'files/+(log|bug)_foo.js', included: false},
138+
{pattern: 'files/{log,bug}_foo.js', included: true}
139+
140+
];
141+
browsers = ['PhantomJS'];
142+
plugins = [
143+
'karma-jasmine',
144+
'karma-phantomjs-launcher'
145+
];
146+
"""
147+
When I start Karma
148+
Then it passes with:
149+
"""
150+
.
151+
PhantomJS
152+
"""
153+

test/e2e/support/files/log_foo.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
console.log('foo')

test/e2e/support/files/test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
describe('plus', function () {
2+
it('should pass', function () {
3+
expect(true).toBe(true)
4+
})
5+
})

0 commit comments

Comments
 (0)