Marcel van Lohuizen | 8a7fc45 | 2018-12-10 15:19:41 +0100 | [diff] [blame] | 1 | // Copyright 2018 The CUE Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package token |
| 16 | |
| 17 | import ( |
| 18 | "fmt" |
| 19 | "math/rand" |
| 20 | "sync" |
| 21 | "testing" |
| 22 | ) |
| 23 | |
| 24 | func checkPos(t *testing.T, msg string, got, want Position) { |
| 25 | if got.Filename != want.Filename { |
| 26 | t.Errorf("%s: got filename = %q; want %q", msg, got.Filename, want.Filename) |
| 27 | } |
| 28 | if got.Offset != want.Offset { |
| 29 | t.Errorf("%s: got offset = %d; want %d", msg, got.Offset, want.Offset) |
| 30 | } |
| 31 | if got.Line != want.Line { |
| 32 | t.Errorf("%s: got line = %d; want %d", msg, got.Line, want.Line) |
| 33 | } |
| 34 | if got.Column != want.Column { |
| 35 | t.Errorf("%s: got column = %d; want %d", msg, got.Column, want.Column) |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | func TestNoPos(t *testing.T) { |
| 40 | if NoPos.IsValid() { |
| 41 | t.Errorf("NoPos should not be valid") |
| 42 | } |
| 43 | var fset *FileSet |
| 44 | checkPos(t, "nil NoPos", fset.Position(NoPos), Position{}) |
| 45 | fset = NewFileSet() |
| 46 | checkPos(t, "fset NoPos", fset.Position(NoPos), Position{}) |
| 47 | } |
| 48 | |
| 49 | var tests = []struct { |
| 50 | filename string |
| 51 | source []byte // may be nil |
| 52 | size int |
| 53 | lines []int |
| 54 | }{ |
| 55 | {"a", []byte{}, 0, []int{}}, |
| 56 | {"b", []byte("01234"), 5, []int{0}}, |
| 57 | {"c", []byte("\n\n\n\n\n\n\n\n\n"), 9, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}}, |
| 58 | {"d", nil, 100, []int{0, 5, 10, 20, 30, 70, 71, 72, 80, 85, 90, 99}}, |
| 59 | {"e", nil, 777, []int{0, 80, 100, 120, 130, 180, 267, 455, 500, 567, 620}}, |
| 60 | {"f", []byte("package p\n\nimport \"fmt\""), 23, []int{0, 10, 11}}, |
| 61 | {"g", []byte("package p\n\nimport \"fmt\"\n"), 24, []int{0, 10, 11}}, |
| 62 | {"h", []byte("package p\n\nimport \"fmt\"\n "), 25, []int{0, 10, 11, 24}}, |
| 63 | } |
| 64 | |
| 65 | func linecol(lines []int, offs int) (int, int) { |
| 66 | prevLineOffs := 0 |
| 67 | for line, lineOffs := range lines { |
| 68 | if offs < lineOffs { |
| 69 | return line, offs - prevLineOffs + 1 |
| 70 | } |
| 71 | prevLineOffs = lineOffs |
| 72 | } |
| 73 | return len(lines), offs - prevLineOffs + 1 |
| 74 | } |
| 75 | |
| 76 | func verifyPositions(t *testing.T, fset *FileSet, f *File, lines []int) { |
| 77 | for offs := 0; offs < f.Size(); offs++ { |
| 78 | p := f.Pos(offs, 0) |
| 79 | offs2 := f.Offset(p) |
| 80 | if offs2 != offs { |
| 81 | t.Errorf("%s, Offset: got offset %d; want %d", f.Name(), offs2, offs) |
| 82 | } |
| 83 | line, col := linecol(lines, offs) |
| 84 | msg := fmt.Sprintf("%s (offs = %d, p = %d)", f.Name(), offs, p) |
| 85 | checkPos(t, msg, f.Position(f.Pos(offs, 0)), Position{f.Name(), offs, line, col}) |
| 86 | checkPos(t, msg, fset.Position(p), Position{f.Name(), offs, line, col}) |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | func makeTestSource(size int, lines []int) []byte { |
| 91 | src := make([]byte, size) |
| 92 | for _, offs := range lines { |
| 93 | if offs > 0 { |
| 94 | src[offs-1] = '\n' |
| 95 | } |
| 96 | } |
| 97 | return src |
| 98 | } |
| 99 | |
| 100 | func TestPositions(t *testing.T) { |
| 101 | const delta = 7 // a non-zero base offset increment |
| 102 | fset := NewFileSet() |
| 103 | for _, test := range tests { |
| 104 | // verify consistency of test case |
| 105 | if test.source != nil && len(test.source) != test.size { |
| 106 | t.Errorf("%s: inconsistent test case: got file size %d; want %d", test.filename, len(test.source), test.size) |
| 107 | } |
| 108 | |
| 109 | // add file and verify name and size |
| 110 | f := fset.AddFile(test.filename, fset.Base()+delta, test.size) |
| 111 | if f.Name() != test.filename { |
| 112 | t.Errorf("got filename %q; want %q", f.Name(), test.filename) |
| 113 | } |
| 114 | if f.Size() != test.size { |
| 115 | t.Errorf("%s: got file size %d; want %d", f.Name(), f.Size(), test.size) |
| 116 | } |
| 117 | if fset.File(f.Pos(0, 0)) != f { |
| 118 | t.Errorf("%s: f.Pos(0, 0) was not found in f", f.Name()) |
| 119 | } |
| 120 | |
| 121 | // add lines individually and verify all positions |
| 122 | for i, offset := range test.lines { |
| 123 | f.AddLine(offset) |
| 124 | if f.LineCount() != i+1 { |
| 125 | t.Errorf("%s, AddLine: got line count %d; want %d", f.Name(), f.LineCount(), i+1) |
| 126 | } |
| 127 | // adding the same offset again should be ignored |
| 128 | f.AddLine(offset) |
| 129 | if f.LineCount() != i+1 { |
| 130 | t.Errorf("%s, AddLine: got unchanged line count %d; want %d", f.Name(), f.LineCount(), i+1) |
| 131 | } |
| 132 | verifyPositions(t, fset, f, test.lines[0:i+1]) |
| 133 | } |
| 134 | |
| 135 | // add lines with SetLines and verify all positions |
| 136 | if ok := f.SetLines(test.lines); !ok { |
| 137 | t.Errorf("%s: SetLines failed", f.Name()) |
| 138 | } |
| 139 | if f.LineCount() != len(test.lines) { |
| 140 | t.Errorf("%s, SetLines: got line count %d; want %d", f.Name(), f.LineCount(), len(test.lines)) |
| 141 | } |
| 142 | verifyPositions(t, fset, f, test.lines) |
| 143 | |
| 144 | // add lines with SetLinesForContent and verify all positions |
| 145 | src := test.source |
| 146 | if src == nil { |
| 147 | // no test source available - create one from scratch |
| 148 | src = makeTestSource(test.size, test.lines) |
| 149 | } |
| 150 | f.SetLinesForContent(src) |
| 151 | if f.LineCount() != len(test.lines) { |
| 152 | t.Errorf("%s, SetLinesForContent: got line count %d; want %d", f.Name(), f.LineCount(), len(test.lines)) |
| 153 | } |
| 154 | verifyPositions(t, fset, f, test.lines) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | func TestLineInfo(t *testing.T) { |
| 159 | fset := NewFileSet() |
| 160 | f := fset.AddFile("foo", fset.Base(), 500) |
| 161 | lines := []int{0, 42, 77, 100, 210, 220, 277, 300, 333, 401} |
| 162 | // add lines individually and provide alternative line information |
| 163 | for _, offs := range lines { |
| 164 | f.AddLine(offs) |
| 165 | f.AddLineInfo(offs, "bar", 42) |
| 166 | } |
| 167 | // verify positions for all offsets |
| 168 | for offs := 0; offs <= f.Size(); offs++ { |
| 169 | p := f.Pos(offs, 0) |
| 170 | _, col := linecol(lines, offs) |
| 171 | msg := fmt.Sprintf("%s (offs = %d, p = %d)", f.Name(), offs, p) |
| 172 | checkPos(t, msg, f.Position(f.Pos(offs, 0)), Position{"bar", offs, 42, col}) |
| 173 | checkPos(t, msg, fset.Position(p), Position{"bar", offs, 42, col}) |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | func TestFiles(t *testing.T) { |
| 178 | fset := NewFileSet() |
| 179 | for i, test := range tests { |
| 180 | base := fset.Base() |
| 181 | if i%2 == 1 { |
| 182 | // Setting a negative base is equivalent to |
| 183 | // fset.Base(), so test some of each. |
| 184 | base = -1 |
| 185 | } |
| 186 | fset.AddFile(test.filename, base, test.size) |
| 187 | j := 0 |
| 188 | fset.Iterate(func(f *File) bool { |
| 189 | if f.Name() != tests[j].filename { |
| 190 | t.Errorf("got filename = %s; want %s", f.Name(), tests[j].filename) |
| 191 | } |
| 192 | j++ |
| 193 | return true |
| 194 | }) |
| 195 | if j != i+1 { |
| 196 | t.Errorf("got %d files; want %d", j, i+1) |
| 197 | } |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | // FileSet.File should return nil if Pos is past the end of the FileSet. |
| 202 | func TestFileSetPastEnd(t *testing.T) { |
| 203 | fset := NewFileSet() |
| 204 | for _, test := range tests { |
| 205 | fset.AddFile(test.filename, fset.Base(), test.size) |
| 206 | } |
| 207 | if f := fset.File(toPos(index(fset.Base()))); f != nil { |
| 208 | t.Errorf("got %v, want nil", f) |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | func TestFileSetCacheUnlikely(t *testing.T) { |
| 213 | fset := NewFileSet() |
| 214 | offsets := make(map[string]index) |
| 215 | for _, test := range tests { |
| 216 | offsets[test.filename] = index(fset.Base()) |
| 217 | fset.AddFile(test.filename, fset.Base(), test.size) |
| 218 | } |
| 219 | for file, pos := range offsets { |
| 220 | f := fset.File(toPos(pos)) |
| 221 | if f.Name() != file { |
| 222 | t.Errorf("got %q at position %d, want %q", f.Name(), pos, file) |
| 223 | } |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | // issue 4345. Test that concurrent use of FileSet.Pos does not trigger a |
| 228 | // race in the FileSet position cache. |
| 229 | func TestFileSetRace(t *testing.T) { |
| 230 | fset := NewFileSet() |
| 231 | for i := 0; i < 100; i++ { |
| 232 | fset.AddFile(fmt.Sprintf("file-%d", i), fset.Base(), 1031) |
| 233 | } |
| 234 | max := int32(fset.Base()) |
| 235 | var stop sync.WaitGroup |
| 236 | r := rand.New(rand.NewSource(7)) |
| 237 | for i := 0; i < 2; i++ { |
| 238 | r := rand.New(rand.NewSource(r.Int63())) |
| 239 | stop.Add(1) |
| 240 | go func() { |
| 241 | for i := 0; i < 1000; i++ { |
| 242 | fset.Position(Pos(r.Int31n(max))) |
| 243 | } |
| 244 | stop.Done() |
| 245 | }() |
| 246 | } |
| 247 | stop.Wait() |
| 248 | } |
| 249 | |
| 250 | // issue 16548. Test that concurrent use of File.AddLine and FileSet.PositionFor |
| 251 | // does not trigger a race in the FileSet position cache. |
| 252 | func TestFileSetRace2(t *testing.T) { |
| 253 | const N = 1e3 |
| 254 | var ( |
| 255 | fset = NewFileSet() |
| 256 | file = fset.AddFile("", -1, N) |
| 257 | ch = make(chan int, 2) |
| 258 | ) |
| 259 | |
| 260 | go func() { |
| 261 | for i := 0; i < N; i++ { |
| 262 | file.AddLine(i) |
| 263 | } |
| 264 | ch <- 1 |
| 265 | }() |
| 266 | |
| 267 | go func() { |
| 268 | pos := file.Pos(0, 0) |
| 269 | for i := 0; i < N; i++ { |
| 270 | fset.PositionFor(pos, false) |
| 271 | } |
| 272 | ch <- 1 |
| 273 | }() |
| 274 | |
| 275 | <-ch |
| 276 | <-ch |
| 277 | } |
| 278 | |
| 279 | func TestPositionFor(t *testing.T) { |
| 280 | src := []byte(` |
| 281 | foo |
| 282 | b |
| 283 | ar |
| 284 | //line :100 |
| 285 | foobar |
| 286 | //line bar:3 |
| 287 | done |
| 288 | `) |
| 289 | |
| 290 | const filename = "foo" |
| 291 | fset := NewFileSet() |
| 292 | f := fset.AddFile(filename, fset.Base(), len(src)) |
| 293 | f.SetLinesForContent(src) |
| 294 | |
| 295 | // verify position info |
| 296 | for i, offs := range f.lines { |
| 297 | got1 := f.PositionFor(f.Pos(int(offs), 0), false) |
| 298 | got2 := f.PositionFor(f.Pos(int(offs), 0), true) |
| 299 | got3 := f.Position(f.Pos(int(offs), 0)) |
| 300 | want := Position{filename, int(offs), i + 1, 1} |
| 301 | checkPos(t, "1. PositionFor unadjusted", got1, want) |
| 302 | checkPos(t, "1. PositionFor adjusted", got2, want) |
| 303 | checkPos(t, "1. Position", got3, want) |
| 304 | } |
| 305 | |
| 306 | // manually add //line info on lines l1, l2 |
| 307 | const l1, l2 = 5, 7 |
| 308 | f.AddLineInfo(int(f.lines[l1-1]), "", 100) |
| 309 | f.AddLineInfo(int(f.lines[l2-1]), "bar", 3) |
| 310 | |
| 311 | // unadjusted position info must remain unchanged |
| 312 | for i, offs := range f.lines { |
| 313 | got1 := f.PositionFor(f.Pos(int(offs), 0), false) |
| 314 | want := Position{filename, int(offs), i + 1, 1} |
| 315 | checkPos(t, "2. PositionFor unadjusted", got1, want) |
| 316 | } |
| 317 | |
| 318 | // adjusted position info should have changed |
| 319 | for i, offs := range f.lines { |
| 320 | got2 := f.PositionFor(f.Pos(int(offs), 0), true) |
| 321 | got3 := f.Position(f.Pos(int(offs), 0)) |
| 322 | want := Position{filename, int(offs), i + 1, 1} |
| 323 | // manually compute wanted filename and line |
| 324 | line := want.Line |
| 325 | if i+1 >= l1 { |
| 326 | want.Filename = "" |
| 327 | want.Line = line - l1 + 100 |
| 328 | } |
| 329 | if i+1 >= l2 { |
| 330 | want.Filename = "bar" |
| 331 | want.Line = line - l2 + 3 |
| 332 | } |
| 333 | checkPos(t, "3. PositionFor adjusted", got2, want) |
| 334 | checkPos(t, "3. Position", got3, want) |
| 335 | } |
| 336 | } |