From 21a646454ddb374111ed7e89e8370c3064a94fef Mon Sep 17 00:00:00 2001 From: Raine Virta Date: Sun, 12 May 2024 01:45:25 +0300 Subject: [PATCH] jest: handle colored output --- Cargo.lock | 2 +- README.md | 4 ++ ghtool/src/commands/test/jest.rs | 64 +++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5c7f8c..5e4566e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1022,7 +1022,7 @@ dependencies = [ [[package]] name = "ghtool" -version = "0.10.3" +version = "0.10.4" dependencies = [ "bytes", "chrono", diff --git a/README.md b/README.md index 53433a8..9adec95 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,10 @@ Any ideas on how to improve this are appreciated. ## Changelog +## 0.10.5 (12.05.2024) + +- jest: Handle colored output. + ## 0.10.3 (11.05.2024) - jest: Allow parsing logs where jest runs using docker-compose. diff --git a/ghtool/src/commands/test/jest.rs b/ghtool/src/commands/test/jest.rs index 3ebcae2..c7ebf4c 100644 --- a/ghtool/src/commands/test/jest.rs +++ b/ghtool/src/commands/test/jest.rs @@ -45,12 +45,22 @@ impl JestLogParser { } fn parse_line(&mut self, raw_line: &str) -> Result<(), eyre::Error> { + let line_no_ansi = String::from_utf8(strip_ansi_escapes::strip(raw_line.as_bytes()))?; let line_no_timestamp = TIMESTAMP.replace(raw_line, ""); match self.state { State::LookingForFail => { - if let Some(caps) = JEST_FAIL_LINE.captures(&line_no_timestamp) { - self.current_fail_start_col = caps.name("fail").unwrap().start(); + if let Some(caps) = JEST_FAIL_LINE.captures(&line_no_ansi) { + // Attempt to find the column where the colored FAIL text starts. + // This column position will be used to determine where jest output starts. + // We can't just take everything after timestamp because there's possibility + // that jest is running inside docker-compose in which case there would be + // service name after timestamp. + // https://github.com/raine/ghtool/assets/11027/c349807a-cad1-45cb-b02f-4d5020bb3c23 + let reset_escape_code = "\u{1b}[0m\u{1b}[7m"; + let reset_pos = line_no_timestamp.find(reset_escape_code); + let fail_pos = line_no_timestamp.find("FAIL"); + self.current_fail_start_col = reset_pos.unwrap_or(fail_pos.unwrap()); let path = caps.name("path").unwrap().as_str().to_string(); // Get line discarding things before the column where FAIL starts let line = line_no_timestamp @@ -353,4 +363,54 @@ mod tests { }] ); } + + #[test] + fn test_colors() { + let logs = r#" +2024-05-11T20:44:13.9945728Z $ jest ./src --color --ci --shard=1/2 +2024-05-11T20:45:16.0032874Z  FAIL  src/test2.test.ts (61.458 s) +2024-05-11T20:45:16.0034300Z test2 +2024-05-11T20:45:16.0037347Z ✓ succeeds (1 ms) +2024-05-11T20:45:16.0038258Z ✕ fails (2 ms) +2024-05-11T20:45:16.0039034Z ✓ foo (60001 ms) +2024-05-11T20:45:16.0039463Z +2024-05-11T20:45:16.0039981Z  ● test2 › fails +2024-05-11T20:45:16.0040506Z +2024-05-11T20:45:16.0041462Z expect(received).toBe(expected) // Object.is equality +2024-05-11T20:45:16.0045857Z +2024-05-11T20:45:16.0046210Z Expected: false +2024-05-11T20:45:16.0046774Z Received: true +2024-05-11T20:45:16.0047256Z  +2024-05-11T20:45:16.0047765Z    5 | +2024-05-11T20:45:16.0048791Z    6 | it("fails", () => { +2024-05-11T20:45:16.0051048Z  > 7 | expect(true).toBe(false); +2024-05-11T20:45:16.0052427Z    | ^ +2024-05-11T20:45:16.0053352Z    8 | }); +2024-05-11T20:45:16.0054060Z    9 | +2024-05-11T20:45:16.0055164Z    10 | it("foo", async () => { +2024-05-11T20:45:16.0056008Z  +2024-05-11T20:45:16.0057064Z  at Object. (src/test2.test.ts:7:18) +2024-05-11T20:45:16.0057817Z +2024-05-11T20:45:16.0064933Z Test Suites: 1 failed, 1 total +2024-05-11T20:45:16.0065943Z Tests: 1 failed, 2 passed, 3 total +2024-05-11T20:45:16.0066489Z Snapshots: 0 total +2024-05-11T20:45:16.0066847Z Time: 61.502 s +2024-05-11T20:45:16.0067359Z Ran all test suites matching /.\/src/i. + "#; + + let failing_tests = JestLogParser::parse(logs).unwrap(); + assert_eq!( + failing_tests, + vec![CheckError { + path: "src/test2.test.ts".to_string(), + lines: vec![ + "\u{1b}[0m\u{1b}[7m\u{1b}[1m\u{1b}[31m FAIL \u{1b}[39m\u{1b}[22m\u{1b}[27m\u{1b}[0m \u{1b}[2msrc/\u{1b}[22m\u{1b}[1mtest2.test.ts\u{1b}[22m (\u{1b}[0m\u{1b}[1m\u{1b}[41m61.458 s\u{1b}[49m\u{1b}[22m\u{1b}[0m)".to_string(), + " test2".to_string(), + " \u{1b}[32m✓\u{1b}[39m \u{1b}[2msucceeds (1 ms)\u{1b}[22m".to_string(), + " \u{1b}[31m✕\u{1b}[39m \u{1b}[2mfails (2 ms)\u{1b}[22m".to_string(), + " \u{1b}[32m✓\u{1b}[39m \u{1b}[2mfoo (60001 ms)\u{1b}[22m".to_string(), + ] + },] + ); + } }