documentation_tester.odin 11 KB

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