documentation_tester.odin 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. package documentation_tester
  2. import "core:os"
  3. import "core:io"
  4. import "core:fmt"
  5. import "core:strings"
  6. import "core:odin/ast"
  7. import "core:odin/parser"
  8. import "core:c/libc"
  9. import doc "core:odin/doc-format"
  10. Example_Test :: struct {
  11. entity_name: string,
  12. package_name: string,
  13. example_code: []string,
  14. expected_output: []string,
  15. skip_output_check: bool,
  16. }
  17. g_header: ^doc.Header
  18. g_bad_doc: bool
  19. g_examples_to_verify: [dynamic]Example_Test
  20. g_path_to_odin: string
  21. array :: proc(a: $A/doc.Array($T)) -> []T {
  22. return doc.from_array(g_header, a)
  23. }
  24. str :: proc(s: $A/doc.String) -> string {
  25. return doc.from_string(g_header, s)
  26. }
  27. common_prefix :: proc(strs: []string) -> string {
  28. if len(strs) == 0 {
  29. return ""
  30. }
  31. n := max(int)
  32. for str in strs {
  33. n = min(n, len(str))
  34. }
  35. prefix := strs[0][:n]
  36. for str in strs[1:] {
  37. for len(prefix) != 0 && str[:len(prefix)] != prefix {
  38. prefix = prefix[:len(prefix)-1]
  39. }
  40. if len(prefix) == 0 {
  41. break
  42. }
  43. }
  44. return prefix
  45. }
  46. errorf :: proc(format: string, args: ..any) -> ! {
  47. fmt.eprintf("%s ", os.args[0])
  48. fmt.eprintf(format, ..args)
  49. fmt.eprintln()
  50. os.exit(1)
  51. }
  52. main :: proc() {
  53. if len(os.args) != 2 {
  54. errorf("expected path to odin executable")
  55. }
  56. g_path_to_odin = os.args[1]
  57. data, ok := os.read_entire_file("all.odin-doc")
  58. if !ok {
  59. errorf("unable to read file: all.odin-doc")
  60. }
  61. err: doc.Reader_Error
  62. g_header, err = doc.read_from_bytes(data)
  63. switch err {
  64. case .None:
  65. case .Header_Too_Small:
  66. errorf("file is too small for the file format")
  67. case .Invalid_Magic:
  68. errorf("invalid magic for the file format")
  69. case .Data_Too_Small:
  70. errorf("data is too small for the file format")
  71. case .Invalid_Version:
  72. errorf("invalid file format version")
  73. }
  74. pkgs := array(g_header.pkgs)
  75. entities := array(g_header.entities)
  76. path_prefix: string
  77. {
  78. fullpaths: [dynamic]string
  79. defer delete(fullpaths)
  80. for pkg in pkgs[1:] {
  81. append(&fullpaths, str(pkg.fullpath))
  82. }
  83. path_prefix = common_prefix(fullpaths[:])
  84. }
  85. for pkg in pkgs[1:] {
  86. entries_array := array(pkg.entries)
  87. fullpath := str(pkg.fullpath)
  88. path := strings.trim_prefix(fullpath, path_prefix)
  89. if ! strings.has_prefix(path, "core/") {
  90. continue
  91. }
  92. trimmed_path := strings.trim_prefix(path, "core/")
  93. if strings.has_prefix(trimmed_path, "sys") {
  94. continue
  95. }
  96. if strings.contains(trimmed_path, "/_") {
  97. continue
  98. }
  99. for entry in entries_array {
  100. entity := entities[entry.entity]
  101. find_and_add_examples(
  102. docs = str(entity.docs),
  103. package_name = str(pkg.name),
  104. entity_name = str(entity.name),
  105. )
  106. }
  107. }
  108. write_test_suite(g_examples_to_verify[:])
  109. if g_bad_doc {
  110. errorf("We created bad documentation!")
  111. }
  112. if ! run_test_suite() {
  113. errorf("Test suite failed!")
  114. }
  115. fmt.println("Examples verified")
  116. }
  117. // NOTE: this is a pretty close copy paste from the website pkg documentation on parsing the docs
  118. find_and_add_examples :: proc(docs: string, package_name: string, entity_name: string) {
  119. if docs == "" {
  120. return
  121. }
  122. Block_Kind :: enum {
  123. Other,
  124. Example,
  125. Output,
  126. }
  127. Block :: struct {
  128. kind: Block_Kind,
  129. lines: []string,
  130. }
  131. lines := strings.split_lines(docs)
  132. curr_block_kind := Block_Kind.Other
  133. start := 0
  134. found_possible_output: bool
  135. example_block: Block // when set the kind should be Example
  136. output_block: Block // when set the kind should be Output
  137. // rely on zii that the kinds have not been set
  138. assert(example_block.kind != .Example)
  139. assert(output_block.kind != .Output)
  140. insert_block :: proc(block: Block, example: ^Block, output: ^Block, name: string) {
  141. switch block.kind {
  142. case .Other:
  143. case .Example:
  144. if example.kind == .Example {
  145. fmt.eprintf("The documentation for %q has multiple examples which is not allowed\n", name)
  146. g_bad_doc = true
  147. }
  148. example^ = block
  149. case .Output: output^ = block
  150. if example.kind == .Output {
  151. fmt.eprintf("The documentation for %q has multiple output which is not allowed\n", name)
  152. g_bad_doc = true
  153. }
  154. output^ = block
  155. }
  156. }
  157. for line, i in lines {
  158. text := strings.trim_space(line)
  159. next_block_kind := curr_block_kind
  160. switch curr_block_kind {
  161. case .Other:
  162. switch {
  163. case strings.has_prefix(line, "Example:"): next_block_kind = .Example
  164. case strings.has_prefix(line, "Output:"): next_block_kind = .Output
  165. case strings.has_prefix(line, "Possible Output:"):
  166. next_block_kind = .Output
  167. found_possible_output = true
  168. }
  169. case .Example:
  170. switch {
  171. case strings.has_prefix(line, "Output:"): next_block_kind = .Output
  172. case strings.has_prefix(line, "Possible Output:"):
  173. next_block_kind = .Output
  174. found_possible_output = true
  175. case ! (text == "" || strings.has_prefix(line, "\t")): next_block_kind = .Other
  176. }
  177. case .Output:
  178. switch {
  179. case strings.has_prefix(line, "Example:"): next_block_kind = .Example
  180. case ! (text == "" || strings.has_prefix(line, "\t")): next_block_kind = .Other
  181. }
  182. }
  183. if i-start > 0 && (curr_block_kind != next_block_kind) {
  184. insert_block(Block{curr_block_kind, lines[start:i]}, &example_block, &output_block, entity_name)
  185. curr_block_kind, start = next_block_kind, i
  186. }
  187. }
  188. if start < len(lines) {
  189. insert_block(Block{curr_block_kind, lines[start:]}, &example_block, &output_block, entity_name)
  190. }
  191. if output_block.kind == .Output && example_block.kind != .Example {
  192. fmt.eprintf("The documentation for %q has an output block but no example\n", entity_name)
  193. g_bad_doc = true
  194. }
  195. // Write example and output block if they're both present
  196. if example_block.kind == .Example && output_block.kind == .Output {
  197. {
  198. // Example block starts with
  199. // `Example:` and a number of white spaces,
  200. lines := &example_block.lines
  201. for len(lines) > 0 && (strings.trim_space(lines[0]) == "" || strings.has_prefix(lines[0], "Example:")) {
  202. lines^ = lines[1:]
  203. }
  204. }
  205. {
  206. // Output block starts with
  207. // `Output:` and a number of white spaces,
  208. // `Possible Output:` and a number of white spaces,
  209. lines := &output_block.lines
  210. for len(lines) > 0 && (strings.trim_space(lines[0]) == "" || strings.has_prefix(lines[0], "Output:") || strings.has_prefix(lines[0], "Possible Output:")) {
  211. lines^ = lines[1:]
  212. }
  213. // Additionally we need to strip all empty lines at the end of output to not include those in the expected output
  214. for len(lines) > 0 && (strings.trim_space(lines[len(lines) - 1]) == "") {
  215. lines^ = lines[:len(lines) - 1]
  216. }
  217. }
  218. // Remove first layer of tabs which are always present
  219. for &line in example_block.lines {
  220. line = strings.trim_prefix(line, "\t")
  221. }
  222. for &line in output_block.lines {
  223. line = strings.trim_prefix(line, "\t")
  224. }
  225. append(&g_examples_to_verify, Example_Test {
  226. entity_name = entity_name,
  227. package_name = package_name,
  228. example_code = example_block.lines,
  229. expected_output = output_block.lines,
  230. skip_output_check = found_possible_output,
  231. })
  232. }
  233. }
  234. write_test_suite :: proc(example_tests: []Example_Test) {
  235. TEST_SUITE_DIRECTORY :: "verify"
  236. os.remove_directory(TEST_SUITE_DIRECTORY)
  237. os.make_directory(TEST_SUITE_DIRECTORY)
  238. example_build := strings.builder_make()
  239. test_runner := strings.builder_make()
  240. strings.write_string(&test_runner,
  241. `//+private
  242. package documentation_verification
  243. import "core:os"
  244. import "core:mem"
  245. import "core:io"
  246. import "core:fmt"
  247. import "core:thread"
  248. import "core:sync"
  249. import "base:intrinsics"
  250. @(private="file")
  251. _read_pipe: os.Handle
  252. @(private="file")
  253. _write_pipe: os.Handle
  254. @(private="file")
  255. _pipe_reader_semaphore: sync.Sema
  256. @(private="file")
  257. _out_data: string
  258. @(private="file")
  259. _out_buffer: [mem.Megabyte]byte
  260. @(private="file")
  261. _bad_test_found: bool
  262. @(private="file")
  263. _spawn_pipe_reader :: proc() {
  264. thread.run(proc() {
  265. stream := os.stream_from_handle(_read_pipe)
  266. reader := io.to_reader(stream)
  267. sync.post(&_pipe_reader_semaphore) // notify thread is ready
  268. for {
  269. n_read := 0
  270. read_to_null_byte := 0
  271. finished_reading := false
  272. for ! finished_reading {
  273. just_read, err := io.read(reader, _out_buffer[n_read:], &n_read); if err != .None {
  274. panic("We got an IO error!")
  275. }
  276. for b in _out_buffer[n_read - just_read: n_read] {
  277. if b == 0 {
  278. finished_reading = true
  279. break
  280. }
  281. read_to_null_byte += 1
  282. }
  283. }
  284. intrinsics.volatile_store(&_out_data, transmute(string)_out_buffer[:read_to_null_byte])
  285. sync.post(&_pipe_reader_semaphore) // notify we read the null byte
  286. }
  287. })
  288. sync.wait(&_pipe_reader_semaphore) // wait for thread to be ready
  289. }
  290. @(private="file")
  291. _check :: proc(test_name: string, expected: string) {
  292. null_byte: [1]byte
  293. os.write(_write_pipe, null_byte[:])
  294. os.flush(_write_pipe)
  295. sync.wait(&_pipe_reader_semaphore)
  296. output := intrinsics.volatile_load(&_out_data) // wait for thread to read null byte
  297. if expected != output {
  298. fmt.eprintf("Test %q got unexpected output:\n%q\n", test_name, output)
  299. fmt.eprintf("Expected:\n%q\n", expected)
  300. _bad_test_found = true
  301. }
  302. }
  303. main :: proc() {
  304. _read_pipe, _write_pipe, _ = os.pipe()
  305. os.stdout = _write_pipe
  306. _spawn_pipe_reader()
  307. `)
  308. Found_Proc :: struct {
  309. name: string,
  310. type: string,
  311. }
  312. found_procedures_for_error_msg: [dynamic]Found_Proc
  313. for test in example_tests {
  314. fmt.printf("--- Generating documentation test for \"%v.%v\"\n", test.package_name, test.entity_name)
  315. clear(&found_procedures_for_error_msg)
  316. strings.builder_reset(&example_build)
  317. strings.write_string(&example_build, "package documentation_verification\n\n")
  318. for line in test.example_code {
  319. strings.write_string(&example_build, line)
  320. strings.write_byte(&example_build, '\n')
  321. }
  322. code_string := strings.to_string(example_build)
  323. example_ast := ast.File { src = code_string }
  324. odin_parser := parser.default_parser()
  325. if ! parser.parse_file(&odin_parser, &example_ast) {
  326. g_bad_doc = true
  327. continue
  328. }
  329. if odin_parser.error_count > 0 {
  330. fmt.eprintf("Errors on the following code generated for %q:\n%v\n", test.entity_name, code_string)
  331. g_bad_doc = true
  332. continue
  333. }
  334. enforced_name := fmt.tprintf("%v_example", test.entity_name)
  335. index_of_proc_name: int
  336. code_test_name: string
  337. for d in example_ast.decls {
  338. value_decl, is_value := d.derived.(^ast.Value_Decl); if ! is_value {
  339. continue
  340. }
  341. if len(value_decl.values) != 1 {
  342. continue
  343. }
  344. proc_lit, is_proc_lit := value_decl.values[0].derived_expr.(^ast.Proc_Lit); if ! is_proc_lit {
  345. continue
  346. }
  347. append(&found_procedures_for_error_msg, Found_Proc {
  348. name = code_string[value_decl.names[0].pos.offset:value_decl.names[0].end.offset],
  349. type = code_string[proc_lit.type.pos.offset:proc_lit.type.end.offset],
  350. })
  351. if len(proc_lit.type.params.list) > 0 {
  352. continue
  353. }
  354. this_procedure_name := code_string[value_decl.names[0].pos.offset:value_decl.names[0].end.offset]
  355. if this_procedure_name != enforced_name {
  356. continue
  357. }
  358. index_of_proc_name = value_decl.names[0].pos.offset
  359. code_test_name = this_procedure_name
  360. break
  361. }
  362. if code_test_name == "" {
  363. fmt.eprintf("We could not find the procedure \"%s :: proc()\" needed to test the example created for \"%s.%s\"\n", enforced_name, test.package_name, test.entity_name)
  364. if len(found_procedures_for_error_msg) > 0{
  365. fmt.eprint("The following procedures were found:\n")
  366. for procedure in found_procedures_for_error_msg {
  367. fmt.eprintf("\t%s :: %s\n", procedure.name, procedure.type)
  368. }
  369. } else {
  370. fmt.eprint("No procedures were found?\n")
  371. }
  372. // NOTE: we don't want to fail the CI in this case, just put the error in the log and test everything else
  373. // g_bad_doc = true
  374. continue
  375. }
  376. // NOTE: packages like 'rand' are random by nature, in these cases we cannot verify against the output string
  377. // in these cases we just mark the output as 'Possible Output' and we simply skip checking against the output
  378. if ! test.skip_output_check {
  379. fmt.sbprintf(&test_runner, "\t%v_%v()\n", test.package_name, code_test_name)
  380. fmt.sbprintf(&test_runner, "\t_check(%q, `", code_test_name)
  381. had_line_error: bool
  382. for line in test.expected_output {
  383. // NOTE: this will escape the multiline string. Even with a backslash it still escapes due to the semantics of `
  384. // I don't think any examples would really need this specific character so let's just make it forbidden and change
  385. // in the future if we really need to
  386. if strings.contains_rune(line, '`') {
  387. fmt.eprintf("The line %q in the output for \"%s.%s\" contains a ` which is not allowed\n", line, test.package_name, test.entity_name)
  388. g_bad_doc = true
  389. had_line_error = true
  390. }
  391. strings.write_string(&test_runner, line)
  392. strings.write_string(&test_runner, "\n")
  393. }
  394. if had_line_error {
  395. continue
  396. }
  397. strings.write_string(&test_runner, "`)\n")
  398. }
  399. save_path := fmt.tprintf("verify/test_%v_%v.odin", test.package_name, code_test_name)
  400. test_file_handle, err := os.open(save_path, os.O_WRONLY | os.O_CREATE); if err != 0 {
  401. fmt.eprintf("We could not open the file to the path %q for writing\n", save_path)
  402. g_bad_doc = true
  403. continue
  404. }
  405. defer os.close(test_file_handle)
  406. stream := os.stream_from_handle(test_file_handle)
  407. writer, ok := io.to_writer(stream); if ! ok {
  408. fmt.eprintf("We could not make the writer for the path %q\n", save_path)
  409. g_bad_doc = true
  410. continue
  411. }
  412. fmt.wprintf(writer, "%v%v_%v", code_string[:index_of_proc_name], test.package_name, code_string[index_of_proc_name:])
  413. fmt.println("Done")
  414. }
  415. strings.write_string(&test_runner,
  416. `
  417. if _bad_test_found {
  418. fmt.eprintln("One or more tests failed")
  419. os.exit(1)
  420. }
  421. }`)
  422. os.write_entire_file("verify/main.odin", transmute([]byte)strings.to_string(test_runner))
  423. }
  424. run_test_suite :: proc() -> bool {
  425. return libc.system(fmt.caprintf("%v run verify", g_path_to_odin)) == 0
  426. }