Skip to content

refactor(model): decompose junit_result.lua into focused collaborators - #322

Open
rcasia wants to merge 11 commits into
mainfrom
review/split-junit-result
Open

rcasia wants to merge 11 commits into
mainfrom
review/split-junit-result

Conversation

@rcasia

@rcasia rcasia commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Decomposes model/junit_result.lua (the largest file in the codebase)
by responsibility. Two rounds:

Round 1 — split pure JUnit-XML parsing concerns out of the class:

  • model/junit_failure.lua: failure/error XML node parsing + stack-trace
    line-number scraping
  • model/junit_status.lua: pass/fail/skip status derivation

Round 2 (based on review feedback) — separated the remaining
file-writing responsibility out of the class entirely:

  • JunitResult had a private create_file_with_content doing real I/O
    (nio.fn.tempname, io.open, write, close), called directly from
    :result()/merge_results(). This tangled 'what happened in this
    test' (domain logic) with 'persist this output for neotest to read' (an
    I/O concern) in the same class.
  • New util/output_writer.lua: the only module that knows about
    io.open/nio.fn.tempname now. Function-as-constructor DI,
    fully unit-testable without touching disk.
  • model/junit_result.lua is now pure — no nio/io requires at all.
    :result()/.merge_results()/SKIPPED/ERROR return plain data
    (status/output_lines/errors/short) instead of a
    neotest.Result with an already-written file path.
  • core/result_builder.lua composes the final neotest.Result via a new
    to_neotest_result(data, output_writer), since it already owned
    tempname_fn in its deps — the natural place for this conversion.
  • Added tests/unit/test_output_writer_spec.lua (new module) and
    tests/unit/test_junit_result_spec.lua (JunitResult had no dedicated
    spec file before this refactor
    — closed that gap).

Result

  • model/junit_result.lua: 324 -> 229 lines, zero I/O
  • New: model/junit_failure.lua (81), model/junit_status.lua (37),
    util/output_writer.lua (69)
  • Public API of JunitResult changed: :result()/.merge_results()/
    SKIPPED/ERROR now return plain data, not a neotest.Result with a
    file path — callers (only result_builder.lua) must pass it through
    output_writer.write(...).
  • 203 test cases, 0 fails. luacheck: 0 warnings across 83 files.

Todo

  • Establish baseline: run full test suite, note pass count, read junit_result.lua fully + its consumers + its existing tests
  • Extract failure/error-node parsing + stack-trace scraping into a focused helper
  • Extract status-derivation logic into a focused helper
  • Reduce duplication between result() and merge_results() by sharing primitives
  • Add focused unit tests for newly extracted modules
  • Run make test, stylua --check, luacheck — all green, same or higher pass count
  • Mark PR ready for review

Follow-up: separate file-writing from JunitResult (per review feedback)

  • Extract a new util/output_writer.lua owning all io.open/nio.fn.tempname calls
  • Make model/junit_result.lua pure (no I/O): result()/merge_results()/SKIPPED/ERROR return plain data
  • core/result_builder.lua composes the final neotest.Result via output_writer
  • core/junit_result_reader.lua no longer threads tempname_fn through to JunitResult
  • Add tests/unit/test_output_writer_spec.lua and tests/unit/test_junit_result_spec.lua (new — no dedicated spec existed before)
  • Update existing tests referencing the removed tempname threading
  • Full suite green (203 cases, 0 fails), luacheck clean (83 files)

@rcasia
rcasia marked this pull request as ready for review September 5, 2026 10:38
JunitResult still had a private create_file_with_content function doing
real I/O (nio.fn.tempname, io.open, write, close), called directly from
:result() and merge_results(). This tangled 'what happened in this test'
(domain logic) with 'persist this output for neotest to read' (I/O
concern) in the same class.

- New neotest-java.util.output_writer: the only module that knows about
  io.open/nio.fn.tempname now. Function-as-constructor DI, injectable
  tempname_fn/open_file/run for fully I/O-free unit testing.
- model/junit_result.lua is now pure: no nio/io requires at all.
  :result() and .merge_results() return plain data
  (status/output_lines/errors/short) instead of a neotest.Result with a
  file path already written. SKIPPED/ERROR do the same.
- core/result_builder.lua composes the final neotest.Result via a new
  to_neotest_result(data, output_writer) helper, calling
  output_writer.write(data.output_lines) — it already owned tempname_fn
  in its deps, so it's the natural place to own this conversion.
- core/junit_result_reader.lua no longer threads tempname_fn through to
  JunitResult:new (JunitResult has no use for it anymore); dropped the
  now-unused nio require.
- Added tests/unit/test_output_writer_spec.lua (new module, fully
  I/O-free via injected open_file/run) and tests/unit/test_junit_result_spec.lua
  (JunitResult had no dedicated spec file before this refactor — now
  covers id/status/errors/output/result/merge_results/SKIPPED/ERROR
  directly, asserting no 'output' filepath leaks out of the pure data
  shape).
- Updated existing reader/result-builder tests that referenced the
  removed tempname threading (JunitResult no longer stores .tempname).

203 test cases, 0 fails. luacheck: 0 warnings across 83 files.
- OutputWriter.write's data param: string | string[] | table -> string
  | table | nil, matching how it's actually called (nil is a valid
  no-op input, and output_lines mixes strings and nested tables, which
  the strict string[] annotation didn't represent).
- JunitResultData.output_lines: string[] -> string | string[], since
  JunitResult.ERROR can set it directly to a raw string (process
  output).
- Added nil-narrowing asserts in test_junit_result_spec.lua before
  indexing :errors() (typed as possibly nil).
@rcasia
rcasia enabled auto-merge (squash) September 5, 2026 11:47
@rcasia
rcasia disabled auto-merge September 5, 2026 11:47
… claims

Per feedback that the new tests weren't helpful/understandable, rewrote
both test_output_writer_spec.lua and test_junit_result_spec.lua:

- test_output_writer_spec.lua: replaced the ad-hoc filepath->content map
  (which required tracing through a mutable closure to understand) with
  a plain ordered  list of {filepath, data} entries returned
  directly from make_writer(). 'joins lines with a separator' and
  'flattens nested tables' previously only checked that both substrings
  existed independently, which would pass even without a separator or
  in the wrong order — now they assert the exact expected concatenated
  string.

- test_junit_result_spec.lua: replaced weak assertions
  (type(x) == "table", eq(nil, data.output) checking a field that never
  existed in the first place) with exact whole-table  checks
  against the full expected shape, so each test documents the complete
  contract instead of a handful of disconnected field checks. Removed
  the misleading 'delegates to junit_status.derive'/'without touching
  disk' framings that didn't actually verify what their names claimed;
  replaced with direct behavioral assertions (PASSED/FAILED + failures
  content, exact merged output ordering, exact error messages).

lua-language-server, stylua, luacheck all clean. Full suite: 204 cases,
0 fails.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant