this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

doc/ref: start testing code blocks which we expect to fail parsing

This is a first taste of code blocks which expect some output,
in which case a failure with errors, and having the test update
the spec.md file accordingly.

We use an HTML comment immediately following the code block,
almost like this is a testscript where we have the annotation to show
the intent of the code block, the code block itself with the input,
and then the result of the operation.

Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
Change-Id: Ia551b291b7c383fb943ab6681bc7951d55465068
Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1232220
Reviewed-by: Marcel van Lohuizen <mpvl@gmail.com>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>

+163 -23
+22 -3
doc/ref/spec.md
··· 64 64 * `cue parse` for CUE files which we expect parse OK, 65 65 but we do not expect to compile or evaluate correctly. 66 66 67 + * `cue ! parse` for CUE which we expect to fail parsing; 68 + the error is recorded as an HTML comment after the code block. 69 + 67 70 * `cue rows` for tables of input-output rows. 68 71 Just like `cue`, we do not yet mark what to do with them, 69 72 but the output column is generally the expected outcome. ··· 493 496 Carriage return characters (`\r`) inside string literals are discarded from 494 497 the string value. 495 498 496 - ```cue untested 499 + ```cue ! parse 497 500 'a\000\xab' 498 501 '\007' 499 502 '\377' ··· 512 515 #"This is an \#(interpolation)"# 513 516 #"The sequence "\U0001F604" renders as \#U0001F604."# 514 517 ``` 518 + <!-- error: 519 + illegal character U+0027 ''' in escape sequence: 520 + spec.md:503:5 521 + escape sequence is invalid Unicode code point: 522 + spec.md:512:3 523 + --> 515 524 516 525 These examples all represent the same string: 517 526 ··· 544 553 To include it is suffices to escape one of the quotes. 545 554 546 555 <!-- TODO: should the backslash here work? --> 547 - ```cue untested 556 + ```cue ! parse 548 557 """ 549 558 lily: 550 559 out of the water ··· 557 566 — Nick Virgilio, Selected Haiku, 1988 558 567 """ 559 568 ``` 569 + <!-- error: 570 + unknown escape sequence: 571 + spec.md:563:14 572 + --> 560 573 561 574 This represents the same string as: 562 575 ··· 1230 1243 _Note_: default constraints of the form `..._` are not yet implemented. 1231 1244 1232 1245 <!-- NOTE: default constraints not yet implemented --> 1233 - ```cue untested 1246 + ```cue ! parse 1234 1247 a: { 1235 1248 foo: string // foo is a string 1236 1249 [=~"^i"]: int // all other fields starting with i are integers ··· 1246 1259 other: "a string" 1247 1260 } 1248 1261 ``` 1262 + <!-- error: 1263 + missing ',' in struct literal: 1264 + spec.md:1253:8 1265 + expected '}', found 'EOF': 1266 + spec.md:1260:3 1267 + --> 1249 1268 1250 1269 <!-- 1251 1270 TODO: are these two equivalent? Rog says that maybe you'll be able to refer
+141 -20
doc/ref/spec_test.go
··· 21 21 "strings" 22 22 "testing" 23 23 24 + "cuelang.org/go/cue/errors" 24 25 "cuelang.org/go/cue/parser" 26 + "cuelang.org/go/internal/cuetest" 25 27 "github.com/yuin/goldmark" 26 28 goldast "github.com/yuin/goldmark/ast" 27 29 "github.com/yuin/goldmark/text" ··· 36 38 md := goldmark.New() 37 39 doc := md.Parser().Parse(text.NewReader(source)) 38 40 39 - walkNode(t, doc, source) 41 + updated := walkNode(t, doc, source) 42 + 43 + if bytes.Equal(source, updated) { 44 + return 45 + } 46 + if cuetest.UpdateGoldenFiles { 47 + if err := os.WriteFile("spec.md", updated, 0o666); err != nil { 48 + t.Fatal(err) 49 + } 50 + } else { 51 + t.Error("spec.md needs updating; run with CUE_UPDATE=1") 52 + } 53 + } 54 + 55 + // walkNode walks the AST and returns the potentially updated source. 56 + // Edits are collected and applied in reverse order to preserve offsets. 57 + func walkNode(t *testing.T, doc goldast.Node, source []byte) []byte { 58 + type edit struct { 59 + offset int // position in source where comment starts or should be inserted 60 + oldLen int // length of existing comment (0 if none) 61 + newStr string 62 + } 63 + var edits []edit 64 + 65 + for child := doc.FirstChild(); child != nil; child = child.NextSibling() { 66 + fcb, ok := child.(*goldast.FencedCodeBlock) 67 + if !ok { 68 + continue 69 + } 70 + e, ok := checkBlock(t, fcb, source) 71 + if ok { 72 + edits = append(edits, edit(e)) 73 + } 74 + } 75 + 76 + // Apply edits in reverse order to preserve earlier offsets. 77 + result := source 78 + for i := len(edits) - 1; i >= 0; i-- { 79 + e := edits[i] 80 + result = append(result[:e.offset], 81 + append([]byte(e.newStr), result[e.offset+e.oldLen:]...)...) 82 + } 83 + return result 40 84 } 41 85 42 - func walkNode(t *testing.T, node goldast.Node, source []byte) { 43 - if fcb, ok := node.(*goldast.FencedCodeBlock); ok { 44 - checkBlock(t, fcb, source) 86 + type blockEdit struct { 87 + offset int 88 + oldLen int 89 + newStr string 90 + } 91 + 92 + // closingFenceEnd returns the byte offset right after the closing fence line (including its newline). 93 + func closingFenceEnd(fcb *goldast.FencedCodeBlock, source []byte) int { 94 + // The content lines end before the closing fence. 95 + // Find the closing ``` after the last content line. 96 + var endOfContent int 97 + if n := fcb.Lines().Len(); n > 0 { 98 + last := fcb.Lines().At(n - 1) 99 + endOfContent = last.Stop 100 + } else { 101 + // Empty code block: closing fence follows the opening fence directly. 102 + // Use the start of the block. 103 + if fcb.Info != nil { 104 + endOfContent = fcb.Info.Segment.Stop 105 + } 106 + } 107 + // Scan forward past the closing ``` line. 108 + idx := bytes.Index(source[endOfContent:], []byte("```")) 109 + if idx < 0 { 110 + return len(source) 111 + } 112 + end := endOfContent + idx + 3 113 + // Skip past the newline after ```. 114 + if end < len(source) && source[end] == '\n' { 115 + end++ 116 + } 117 + return end 118 + } 119 + 120 + const commentPrefix = "<!-- error:" 121 + 122 + // existingComment checks if an HTML comment with error info exists 123 + // right after the code block's closing fence. Returns the comment length and content. 124 + func existingComment(source []byte, offset int) (length int, content string) { 125 + rest := source[offset:] 126 + if !bytes.HasPrefix(rest, []byte(commentPrefix)) { 127 + return 0, "" 45 128 } 46 - for child := node.FirstChild(); child != nil; child = child.NextSibling() { 47 - walkNode(t, child, source) 129 + endIdx := bytes.Index(rest, []byte("-->")) 130 + if endIdx < 0 { 131 + return 0, "" 132 + } 133 + end := endIdx + 3 134 + // Gobble the newline after --> if present. 135 + if end < len(rest) && rest[end] == '\n' { 136 + end++ 48 137 } 138 + return end, string(rest[:end]) 49 139 } 50 140 51 - func checkBlock(t *testing.T, fcb *goldast.FencedCodeBlock, source []byte) { 141 + func formatComment(errStr string) string { 142 + return commentPrefix + "\n" + errStr + "-->\n" 143 + } 144 + 145 + func checkBlock(t *testing.T, fcb *goldast.FencedCodeBlock, source []byte) (blockEdit, bool) { 52 146 if fcb.Info == nil { 53 - return 147 + return blockEdit{}, false 54 148 } 55 149 info := string(fcb.Info.Value(source)) 56 150 fields := strings.Fields(info) 57 151 if len(fields) == 0 { 58 - return 152 + return blockEdit{}, false 59 153 } 60 154 61 155 // Compute the markdown line number for the opening ``` line. ··· 64 158 startOffset := fcb.Lines().At(0).Start 65 159 mdLine = bytes.Count(source[:startOffset], []byte("\n")) 66 160 } 67 - pos := fmt.Sprintf("spec.md:%d", mdLine) 161 + blockPos := fmt.Sprintf("spec.md:%d", mdLine) 68 162 69 163 lang := fields[0] 70 164 if lang != "cue" { 71 - return // skip non-CUE code blocks 165 + return blockEdit{}, false 72 166 } 73 167 168 + // A "!" before the mode negates it, expecting failure. 169 + rest := fields[1:] 170 + wantError := len(rest) > 0 && rest[0] == "!" 171 + if wantError { 172 + rest = rest[1:] 173 + } 74 174 // By default, we check for valid syntax. 75 175 // TODO: mark intent (export, eval, vet) and validate it. 76 176 mode := "parse" 77 - if len(fields) > 1 { 78 - mode = fields[1] 177 + if len(rest) > 0 { 178 + mode = rest[0] 79 179 } 80 180 switch mode { 81 181 case "parse": 82 182 case "rows": 83 183 // TODO: parse and validate line by line 84 - return 184 + return blockEdit{}, false 85 185 case "untested": 86 - return // intentionally not tested 186 + return blockEdit{}, false 87 187 default: 88 - t.Errorf("%s: unknown cue code block mode: ```%s", pos, info) 89 - return 188 + t.Errorf("%s: unknown cue code block mode: ```%s", blockPos, info) 189 + return blockEdit{}, false 90 190 } 91 191 92 192 var buf bytes.Buffer 193 + // Prepend newlines so the parser reports correct markdown line numbers. 194 + for range mdLine { 195 + buf.WriteByte('\n') 196 + } 93 197 for i := 0; i < fcb.Lines().Len(); i++ { 94 198 line := fcb.Lines().At(i) 95 199 buf.Write(line.Value(source)) 96 200 } 97 201 src := buf.String() 98 202 99 - _, err := parser.ParseFile(pos, src, parser.ParseComments) 100 - if err != nil { 101 - t.Errorf("%s: %q block failed to parse:\n%s", pos, info, err) 203 + _, err := parser.ParseFile("spec.md", src, parser.ParseComments) 204 + 205 + if !wantError { 206 + if err != nil { 207 + t.Errorf("%s: %q block failed to parse:\n%s", blockPos, info, err) 208 + } 209 + return blockEdit{}, false 102 210 } 211 + if err == nil { 212 + t.Errorf("%s: %q block parsed successfully, but expected an error", blockPos, info) 213 + return blockEdit{}, false 214 + } 215 + // Check or update the error comment following the code block. 216 + fenceEnd := closingFenceEnd(fcb, source) 217 + oldLen, _ := existingComment(source, fenceEnd) 218 + want := formatComment(errors.Details(err, nil)) 219 + return blockEdit{ 220 + offset: fenceEnd, 221 + oldLen: oldLen, 222 + newStr: want, 223 + }, true 103 224 }