ScanRun.java

1
// SPDX-License-Identifier: Apache-2.0
2
// Copyright 2026 Egothor
3
// Copyright 2026 Accenture
4
package org.egothor.methodatlas;
5
6
import java.security.MessageDigest;
7
import java.security.NoSuchAlgorithmException;
8
import java.time.Instant;
9
import java.util.HexFormat;
10
import java.util.Objects;
11
import java.util.UUID;
12
13
/**
14
 * Immutable record identifying one CLI invocation of MethodAtlas.
15
 *
16
 * <p>
17
 * Created once at the top of {@link MethodAtlasApp#run(String[], java.io.PrintWriter)}
18
 * and propagated through {@link ScanRunContext} so that every log record
19
 * emitted during the run can carry the correlation id. This makes it
20
 * possible to trace a single audit-trail artefact, a single SARIF file, or a
21
 * single CI log slice back to the exact run that produced it.
22
 * </p>
23
 *
24
 * <h2>Fields</h2>
25
 *
26
 * <ul>
27
 *   <li>{@code runId} — a short hex-encoded random id (16 hex characters);
28
 *       intentionally not a full UUID because the id appears in log lines
29
 *       and CSV preamble where 36-character UUIDs are noisy.</li>
30
 *   <li>{@code startedAt} — wall-clock timestamp at run construction.</li>
31
 *   <li>{@code toolVersion} — implementation version reported by the
32
 *       {@code methodatlas} JAR manifest; falls back to {@code "dev"} for
33
 *       developer builds.</li>
34
 *   <li>{@code configFingerprint} — SHA-256 of a stable textual rendering of
35
 *       the parsed {@link CliConfig}. Lets two runs of the same configuration
36
 *       be correlated across time and machine boundaries.</li>
37
 * </ul>
38
 *
39
 * @param runId             short hex correlation id, non-empty
40
 * @param startedAt         wall-clock time the run was started
41
 * @param toolVersion       implementation version or {@code "dev"}
42
 * @param configFingerprint SHA-256 hex digest of the canonical configuration
43
 *                          text; never {@code null}, never empty
44
 * @since 1.0.0
45
 */
46
public record ScanRun(String runId, Instant startedAt, String toolVersion, String configFingerprint) {
47
48
    /**
49
     * Compact constructor enforcing the documented invariants on the
50
     * component values.
51
     */
52
    public ScanRun {
53
        Objects.requireNonNull(runId, "runId");
54
        Objects.requireNonNull(startedAt, "startedAt");
55
        Objects.requireNonNull(toolVersion, "toolVersion");
56
        Objects.requireNonNull(configFingerprint, "configFingerprint");
57
        if (runId.isBlank()) {
58
            throw new IllegalArgumentException("runId must not be blank");
59
        }
60
        if (configFingerprint.isBlank()) {
61
            throw new IllegalArgumentException("configFingerprint must not be blank");
62
        }
63
    }
64
65
    /**
66
     * Creates a new {@code ScanRun} with a freshly generated correlation id
67
     * and the current wall-clock timestamp.
68
     *
69
     * <p>
70
     * The {@code configFingerprint} is the SHA-256 of {@code configText}, a
71
     * stable textual rendering of the parsed CLI configuration. Two runs
72
     * with byte-identical configuration produce the same fingerprint,
73
     * letting operators correlate scans without sharing the full config.
74
     * </p>
75
     *
76
     * @param toolVersion implementation version, or {@code "dev"} for
77
     *                    builds without a manifest version
78
     * @param configText  canonical textual rendering of the parsed
79
     *                    {@link CliConfig}; must not be {@code null}
80
     * @return a fresh scan-run identifier
81
     */
82
    public static ScanRun create(String toolVersion, String configText) {
83
        String runId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
84 1 1. create : replaced return value with null for org/egothor/methodatlas/ScanRun::create → KILLED
        return new ScanRun(runId, Instant.now(),
85 4 1. create : removed conditional - replaced equality check with true → SURVIVED
2. create : removed conditional - replaced equality check with false → SURVIVED
3. create : removed conditional - replaced equality check with false → KILLED
4. create : removed conditional - replaced equality check with true → KILLED
                toolVersion == null || toolVersion.isBlank() ? "dev" : toolVersion,
86
                sha256(configText));
87
    }
88
89
    private static String sha256(String value) {
90
        try {
91
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
92 1 1. sha256 : replaced return value with "" for org/egothor/methodatlas/ScanRun::sha256 → KILLED
            return HexFormat.of().formatHex(digest.digest(
93 2 1. sha256 : removed conditional - replaced equality check with false → SURVIVED
2. sha256 : removed conditional - replaced equality check with true → KILLED
                    value == null ? new byte[0] : value.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
94
        } catch (NoSuchAlgorithmException e) {
95
            throw new IllegalStateException("SHA-256 not available", e);
96
        }
97
    }
98
}

Mutations

84

1.1
Location : create
Killed by : org.egothor.methodatlas.ScanRunTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ScanRunTest]/[method:create_consecutiveCalls_produceDistinctIds()]
replaced return value with null for org/egothor/methodatlas/ScanRun::create → KILLED

85

1.1
Location : create
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

2.2
Location : create
Killed by : org.egothor.methodatlas.ScanRunTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ScanRunTest]/[method:create_blankToolVersion_fallsBackToDev()]
removed conditional - replaced equality check with false → KILLED

3.3
Location : create
Killed by : org.egothor.methodatlas.ScanRunTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ScanRunTest]/[method:create_nullToolVersion_fallsBackToDev()]
removed conditional - replaced equality check with true → KILLED

4.4
Location : create
Killed by : none
removed conditional - replaced equality check with false → SURVIVED Covering tests

92

1.1
Location : sha256
Killed by : org.egothor.methodatlas.ScanRunTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ScanRunTest]/[method:create_consecutiveCalls_produceDistinctIds()]
replaced return value with "" for org/egothor/methodatlas/ScanRun::sha256 → KILLED

93

1.1
Location : sha256
Killed by : org.egothor.methodatlas.ScanRunTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ScanRunTest]/[method:create_differentConfigText_producesDifferentFingerprint()]
removed conditional - replaced equality check with true → KILLED

2.2
Location : sha256
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

Active mutators

Tests examined


Report generated by PIT 1.22.1