reporting.odin 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. //+private
  2. package testing
  3. /*
  4. (c) Copyright 2024 Feoramund <[email protected]>.
  5. Made available under Odin's BSD-3 license.
  6. List of contributors:
  7. Feoramund: Total rewrite.
  8. */
  9. import "base:runtime"
  10. import "core:encoding/ansi"
  11. import "core:fmt"
  12. import "core:io"
  13. import "core:mem"
  14. import "core:path/filepath"
  15. import "core:strings"
  16. // Definitions of colors for use in the test runner.
  17. SGR_RESET :: ansi.CSI + ansi.RESET + ansi.SGR
  18. SGR_READY :: ansi.CSI + ansi.FG_BRIGHT_BLACK + ansi.SGR
  19. SGR_RUNNING :: ansi.CSI + ansi.FG_YELLOW + ansi.SGR
  20. SGR_SUCCESS :: ansi.CSI + ansi.FG_GREEN + ansi.SGR
  21. SGR_FAILED :: ansi.CSI + ansi.FG_RED + ansi.SGR
  22. MAX_PROGRESS_WIDTH :: 100
  23. // More than enough bytes to cover long package names, long test names, dozens
  24. // of ANSI codes, et cetera.
  25. LINE_BUFFER_SIZE :: (MAX_PROGRESS_WIDTH * 8 + 224) * runtime.Byte
  26. PROGRESS_COLUMN_SPACING :: 2
  27. Package_Run :: struct {
  28. name: string,
  29. header: string,
  30. frame_ready: bool,
  31. redraw_buffer: [LINE_BUFFER_SIZE]byte,
  32. redraw_string: string,
  33. last_change_state: Test_State,
  34. last_change_name: string,
  35. tests: []Internal_Test,
  36. test_states: []Test_State,
  37. }
  38. Report :: struct {
  39. packages: []Package_Run,
  40. packages_by_name: map[string]^Package_Run,
  41. pkg_column_len: int,
  42. test_column_len: int,
  43. progress_width: int,
  44. all_tests: []Internal_Test,
  45. all_test_states: []Test_State,
  46. }
  47. // Organize all tests by package and sort out test state data.
  48. make_report :: proc(internal_tests: []Internal_Test) -> (report: Report, error: runtime.Allocator_Error) {
  49. assert(len(internal_tests) > 0, "make_report called with no tests")
  50. packages: [dynamic]Package_Run
  51. report.all_tests = internal_tests
  52. report.all_test_states = make([]Test_State, len(internal_tests)) or_return
  53. // First, figure out what belongs where.
  54. #no_bounds_check cur_pkg := internal_tests[0].pkg
  55. pkg_start: int
  56. // This loop assumes the tests are sorted by package already.
  57. for it, index in internal_tests {
  58. if cur_pkg != it.pkg {
  59. #no_bounds_check {
  60. append(&packages, Package_Run {
  61. name = cur_pkg,
  62. tests = report.all_tests[pkg_start:index],
  63. test_states = report.all_test_states[pkg_start:index],
  64. }) or_return
  65. }
  66. when PROGRESS_WIDTH == 0 {
  67. report.progress_width = max(report.progress_width, index - pkg_start)
  68. }
  69. pkg_start = index
  70. report.pkg_column_len = max(report.pkg_column_len, len(cur_pkg))
  71. cur_pkg = it.pkg
  72. }
  73. report.test_column_len = max(report.test_column_len, len(it.name))
  74. }
  75. // Handle the last (or only) package.
  76. #no_bounds_check {
  77. append(&packages, Package_Run {
  78. name = cur_pkg,
  79. header = cur_pkg,
  80. tests = report.all_tests[pkg_start:],
  81. test_states = report.all_test_states[pkg_start:],
  82. }) or_return
  83. }
  84. when PROGRESS_WIDTH == 0 {
  85. report.progress_width = max(report.progress_width, len(internal_tests) - pkg_start)
  86. } else {
  87. report.progress_width = PROGRESS_WIDTH
  88. }
  89. report.progress_width = min(report.progress_width, MAX_PROGRESS_WIDTH)
  90. report.pkg_column_len = PROGRESS_COLUMN_SPACING + max(report.pkg_column_len, len(cur_pkg))
  91. shrink(&packages) or_return
  92. for &pkg in packages {
  93. pkg.header = fmt.aprintf("%- *[1]s[", pkg.name, report.pkg_column_len)
  94. assert(len(pkg.header) > 0, "Error allocating package header string.")
  95. // This is safe because the array is done resizing, and it has the same
  96. // lifetime as the map.
  97. report.packages_by_name[pkg.name] = &pkg
  98. }
  99. // It's okay to discard the dynamic array's allocator information here,
  100. // because its capacity has been shrunk to its length, it was allocated by
  101. // the caller's context allocator, and it will be deallocated by the same.
  102. //
  103. // `delete_slice` is equivalent to `delete_dynamic_array` in this case.
  104. report.packages = packages[:]
  105. return
  106. }
  107. destroy_report :: proc(report: ^Report) {
  108. for pkg in report.packages {
  109. delete(pkg.header)
  110. }
  111. delete(report.packages)
  112. delete(report.packages_by_name)
  113. delete(report.all_test_states)
  114. }
  115. redraw_package :: proc(w: io.Writer, report: Report, pkg: ^Package_Run) {
  116. if pkg.frame_ready {
  117. io.write_string(w, pkg.redraw_string)
  118. return
  119. }
  120. // Write the output line here so we can cache it.
  121. line_builder := strings.builder_from_bytes(pkg.redraw_buffer[:])
  122. line_writer := strings.to_writer(&line_builder)
  123. highest_run_index: int
  124. failed_count: int
  125. done_count: int
  126. #no_bounds_check for i := 0; i < len(pkg.test_states); i += 1 {
  127. switch pkg.test_states[i] {
  128. case .Ready:
  129. continue
  130. case .Running:
  131. highest_run_index = max(highest_run_index, i)
  132. case .Successful:
  133. done_count += 1
  134. case .Failed:
  135. failed_count += 1
  136. done_count += 1
  137. }
  138. }
  139. start := max(0, highest_run_index - (report.progress_width - 1))
  140. end := min(start + report.progress_width, len(pkg.test_states))
  141. // This variable is to keep track of the last ANSI code emitted, in
  142. // order to avoid repeating the same code over in a sequence.
  143. //
  144. // This should help reduce screen flicker.
  145. last_state := Test_State(-1)
  146. io.write_string(line_writer, pkg.header)
  147. #no_bounds_check for state in pkg.test_states[start:end] {
  148. switch state {
  149. case .Ready:
  150. if last_state != state {
  151. io.write_string(line_writer, SGR_READY)
  152. last_state = state
  153. }
  154. case .Running:
  155. if last_state != state {
  156. io.write_string(line_writer, SGR_RUNNING)
  157. last_state = state
  158. }
  159. case .Successful:
  160. if last_state != state {
  161. io.write_string(line_writer, SGR_SUCCESS)
  162. last_state = state
  163. }
  164. case .Failed:
  165. if last_state != state {
  166. io.write_string(line_writer, SGR_FAILED)
  167. last_state = state
  168. }
  169. }
  170. io.write_byte(line_writer, '|')
  171. }
  172. for _ in 0 ..< report.progress_width - (end - start) {
  173. io.write_byte(line_writer, ' ')
  174. }
  175. io.write_string(line_writer, SGR_RESET + "] ")
  176. ticker: string
  177. if done_count == len(pkg.test_states) {
  178. ticker = "[package done]"
  179. if failed_count > 0 {
  180. ticker = fmt.tprintf("%s (" + SGR_FAILED + "%i" + SGR_RESET + " failed)", ticker, failed_count)
  181. }
  182. } else {
  183. if len(pkg.last_change_name) == 0 {
  184. #no_bounds_check pkg.last_change_name = pkg.tests[0].name
  185. }
  186. switch pkg.last_change_state {
  187. case .Ready:
  188. ticker = fmt.tprintf(SGR_READY + "%s" + SGR_RESET, pkg.last_change_name)
  189. case .Running:
  190. ticker = fmt.tprintf(SGR_RUNNING + "%s" + SGR_RESET, pkg.last_change_name)
  191. case .Failed:
  192. ticker = fmt.tprintf(SGR_FAILED + "%s" + SGR_RESET, pkg.last_change_name)
  193. case .Successful:
  194. ticker = fmt.tprintf(SGR_SUCCESS + "%s" + SGR_RESET, pkg.last_change_name)
  195. }
  196. }
  197. if done_count == len(pkg.test_states) {
  198. fmt.wprintfln(line_writer, " % 4i :: %s",
  199. len(pkg.test_states),
  200. ticker,
  201. )
  202. } else {
  203. fmt.wprintfln(line_writer, "% 4i/% 4i :: %s",
  204. done_count,
  205. len(pkg.test_states),
  206. ticker,
  207. )
  208. }
  209. pkg.redraw_string = strings.to_string(line_builder)
  210. pkg.frame_ready = true
  211. io.write_string(w, pkg.redraw_string)
  212. }
  213. redraw_report :: proc(w: io.Writer, report: Report) {
  214. // If we print a line longer than the user's terminal can handle, it may
  215. // wrap around, shifting the progress report out of alignment.
  216. //
  217. // There are ways to get the current terminal width, and that would be the
  218. // ideal way to handle this, but it would require system-specific code such
  219. // as setting STDIN to be non-blocking in order to read the response from
  220. // the ANSI DSR escape code, or reading environment variables.
  221. //
  222. // The DECAWM escape codes control whether or not the terminal will wrap
  223. // long lines or overwrite the last visible character.
  224. // This should be fine for now.
  225. //
  226. // Note that we only do this for the animated summary; log messages are
  227. // still perfectly fine to wrap, as they're printed in their own batch,
  228. // whereas the animation depends on each package being only on one line.
  229. //
  230. // Of course, if you resize your terminal while it's printing, things can
  231. // still break...
  232. fmt.wprint(w, ansi.CSI + ansi.DECAWM_OFF)
  233. for &pkg in report.packages {
  234. redraw_package(w, report, &pkg)
  235. }
  236. fmt.wprint(w, ansi.CSI + ansi.DECAWM_ON)
  237. }
  238. needs_to_redraw :: proc(report: Report) -> bool {
  239. for pkg in report.packages {
  240. if !pkg.frame_ready {
  241. return true
  242. }
  243. }
  244. return false
  245. }
  246. draw_status_bar :: proc(w: io.Writer, threads_string: string, total_done_count, total_test_count: int) {
  247. if total_done_count == total_test_count {
  248. // All tests are done; print a blank line to maintain the same height
  249. // of the progress report.
  250. fmt.wprintln(w)
  251. } else {
  252. fmt.wprintfln(w,
  253. "%s % 4i/% 4i :: total",
  254. threads_string,
  255. total_done_count,
  256. total_test_count)
  257. }
  258. }
  259. write_memory_report :: proc(w: io.Writer, tracker: ^mem.Tracking_Allocator, pkg, name: string) {
  260. fmt.wprintf(w,
  261. "<% 10M/% 10M> <% 10M> (% 5i/% 5i) :: %s.%s",
  262. tracker.current_memory_allocated,
  263. tracker.total_memory_allocated,
  264. tracker.peak_memory_allocated,
  265. tracker.total_free_count,
  266. tracker.total_allocation_count,
  267. pkg,
  268. name)
  269. for ptr, entry in tracker.allocation_map {
  270. fmt.wprintf(w,
  271. "\n +++ leak % 10M @ %p [%s:%i:%s()]",
  272. entry.size,
  273. ptr,
  274. filepath.base(entry.location.file_path),
  275. entry.location.line,
  276. entry.location.procedure)
  277. }
  278. for entry in tracker.bad_free_array {
  279. fmt.wprintf(w,
  280. "\n +++ bad free @ %p [%s:%i:%s()]",
  281. entry.memory,
  282. filepath.base(entry.location.file_path),
  283. entry.location.line,
  284. entry.location.procedure)
  285. }
  286. }