ScanCommand.java

1
package org.egothor.methodatlas.command;
2
3
import java.io.IOException;
4
import java.io.PrintWriter;
5
import java.nio.file.Path;
6
import java.nio.file.Paths;
7
import java.time.Instant;
8
import java.util.List;
9
import java.util.logging.Level;
10
import java.util.logging.Logger;
11
12
import org.egothor.methodatlas.AiResultCache;
13
import org.egothor.methodatlas.emit.ClassificationOverride;
14
import org.egothor.methodatlas.CliConfig;
15
import org.egothor.methodatlas.emit.OutputMode;
16
import org.egothor.methodatlas.emit.TestMethodSink;
17
import org.egothor.methodatlas.ai.AiSuggestionEngine;
18
import org.egothor.methodatlas.api.TestDiscovery;
19
import org.egothor.methodatlas.api.TestDiscoveryConfig;
20
import org.egothor.methodatlas.emit.OutputEmitter;
21
22
/**
23
 * CLI command handler for the default CSV and {@code -plain} output modes.
24
 *
25
 * <p>
26
 * Scans one or more source roots, optionally enriches the output with AI
27
 * suggestions, and emits test-method records incrementally to the supplied
28
 * writer. When {@link CliConfig#detectSecrets()} is enabled, deterministic
29
 * credential detection runs after the scan and writes a dedicated secrets CSV
30
 * (see {@link CredentialDetectionRunner}).
31
 * </p>
32
 *
33
 * @see org.egothor.methodatlas.emit.OutputEmitter
34
 * @see SarifCommand
35
 * @see GitHubAnnotationsCommand
36
 */
37
public final class ScanCommand implements Command {
38
39
    private static final Logger LOG = Logger.getLogger(ScanCommand.class.getName());
40
41
    private final CliConfig cliConfig;
42
    private final TestDiscoveryConfig discoveryConfig;
43
    private final AiSuggestionEngine aiEngine;
44
    private final ClassificationOverride override;
45
    private final AiResultCache aiCache;
46
    private final PluginLoader pluginLoader;
47
    private final ScanOrchestrator scanOrchestrator;
48
49
    /**
50
     * Creates a new scan command.
51
     *
52
     * @param cliConfig         full parsed CLI configuration
53
     * @param discoveryConfig   discovery configuration forwarded to providers
54
     * @param aiEngine          AI engine providing suggestions; {@code null}
55
     *                          when AI is disabled
56
     * @param override          human classification overrides
57
     * @param aiCache           AI result cache
58
     * @param pluginLoader      loader used to resolve and close
59
     *                          {@link TestDiscovery} providers
60
     * @param scanOrchestrator  scan-and-emit orchestrator used to process
61
     *                          each root with the configured per-record sink
62
     */
63
    public ScanCommand(CliConfig cliConfig, TestDiscoveryConfig discoveryConfig,
64
            AiSuggestionEngine aiEngine, ClassificationOverride override,
65
            AiResultCache aiCache, PluginLoader pluginLoader,
66
            ScanOrchestrator scanOrchestrator) {
67
        this.cliConfig = cliConfig;
68
        this.discoveryConfig = discoveryConfig;
69
        this.aiEngine = aiEngine;
70
        this.override = override;
71
        this.aiCache = aiCache;
72
        this.pluginLoader = pluginLoader;
73
        this.scanOrchestrator = scanOrchestrator;
74
    }
75
76
    /**
77
     * Runs the scan and emits output incrementally.
78
     *
79
     * @param out writer that receives all emitted output
80
     * @return {@code 0} if all files were processed successfully, {@code 1} if
81
     *         any file produced a parse or processing error
82
     * @throws IOException if traversing a file tree fails
83
     */
84
    @Override
85
    @SuppressWarnings("PMD.NPathComplexity") // combinatorial expansion of optional CSV columns; inherent to the format
86
    public int execute(PrintWriter out) throws IOException {
87 2 1. execute : removed conditional - replaced equality check with true → KILLED
2. execute : removed conditional - replaced equality check with false → KILLED
        boolean aiEnabled = aiEngine != null;
88 4 1. execute : removed conditional - replaced equality check with true → SURVIVED
2. execute : removed conditional - replaced equality check with true → KILLED
3. execute : removed conditional - replaced equality check with false → KILLED
4. execute : removed conditional - replaced equality check with false → KILLED
        boolean confidenceEnabled = aiEnabled && cliConfig.aiOptions().confidence();
89
        boolean contentHashEnabled = cliConfig.contentHash();
90 2 1. execute : removed conditional - replaced equality check with false → SURVIVED
2. execute : removed conditional - replaced equality check with true → TIMED_OUT
        List<Path> roots = cliConfig.paths().isEmpty() ? List.of(Paths.get(".")) : cliConfig.paths();
91
92
        OutputEmitter emitter = new OutputEmitter(out, aiEnabled, confidenceEnabled, contentHashEnabled,
93
                cliConfig.driftDetect(), cliConfig.emitSourceRoot());
94
95 2 1. execute : removed conditional - replaced equality check with false → KILLED
2. execute : removed conditional - replaced equality check with true → KILLED
        if (cliConfig.emitMetadata()) {
96
            String version = ScanCommand.class.getPackage().getImplementationVersion();
97
            String taxonomyInfo = new AiRuntimeBuilder().resolveTaxonomyInfo(cliConfig.aiOptions(), aiEnabled);
98 3 1. execute : removed conditional - replaced equality check with false → SURVIVED
2. execute : removed conditional - replaced equality check with true → SURVIVED
3. execute : removed call to org/egothor/methodatlas/emit/OutputEmitter::emitMetadata → KILLED
            emitter.emitMetadata(version != null ? version : "dev", Instant.now().toString(), taxonomyInfo);
99
        }
100
101 1 1. execute : removed call to org/egothor/methodatlas/emit/OutputEmitter::emitCsvHeader → KILLED
        emitter.emitCsvHeader(cliConfig.outputMode());
102
103
        final OutputMode mode = cliConfig.outputMode();
104
        final boolean emitSourceRoot = cliConfig.emitSourceRoot();
105
106
        // Credential detection: prepare() decides whether to fold triage into the
107
        // per-class classification call (so the class source is sent once) and, when
108
        // folding, returns the context the scan threads through; finish() emits.
109 2 1. execute : removed conditional - replaced equality check with false → SURVIVED
2. execute : removed conditional - replaced equality check with true → KILLED
        CredentialDetectionRunner secretRunner = cliConfig.detectSecrets()
110
                ? new CredentialDetectionRunner(cliConfig, discoveryConfig, pluginLoader, scanOrchestrator, aiEngine)
111
                : null;
112 2 1. execute : removed conditional - replaced equality check with false → SURVIVED
2. execute : removed conditional - replaced equality check with true → KILLED
        CredentialTriageContext secretCtx = secretRunner != null ? secretRunner.prepare(roots) : null;
113
114
        // Scan each root with its own sink so the source_root value can be captured
115
        // per root. When emitSourceRoot is false, sourceRoot is null and the column
116
        // is omitted from the output.
117
        List<TestDiscovery> providers = pluginLoader.loadProviders(discoveryConfig);
118
        boolean hadErrors = false;
119
        try {
120
            for (Path root : roots) {
121 2 1. execute : removed conditional - replaced equality check with false → SURVIVED
2. execute : removed conditional - replaced equality check with true → SURVIVED
                String sourceRoot = emitSourceRoot ? ContentHasher.filePrefix(List.of(root)) : null;
122
                TestMethodSink rootSink = (fqcn, method, beginLine, loc, contentHash, tags, displayName, suggestion) ->
123 1 1. lambda$execute$0 : removed call to org/egothor/methodatlas/emit/OutputEmitter::emit → KILLED
                        emitter.emit(mode, fqcn, method, loc, contentHash, tags, displayName, suggestion, sourceRoot);
124 2 1. execute : removed conditional - replaced equality check with false → KILLED
2. execute : removed conditional - replaced equality check with true → KILLED
                if (scanOrchestrator.runDiscovery(root, providers, cliConfig.aiOptions(), aiEngine,
125
                        scanOrchestrator.filterSink(rootSink, cliConfig.securityOnly(),
126
                                cliConfig.minConfidence(), confidenceEnabled),
127
                        cliConfig.contentHash(), override, aiCache, secretCtx)) {
128
                    hadErrors = true;
129
                }
130
            }
131
        } finally {
132 1 1. execute : removed call to org/egothor/methodatlas/command/PluginLoader::closeAll → SURVIVED
            pluginLoader.closeAll(providers);
133
        }
134
135
        // Surface any write error the streaming PrintWriter swallowed, so a
136
        // truncated report (broken pipe, full disk) is never reported as success.
137 1 1. execute : removed call to org/egothor/methodatlas/emit/OutputEmitter::finish → SURVIVED
        emitter.finish();
138
139
        if (aiCache.isActive() && LOG.isLoggable(Level.INFO)) {
140
            LOG.log(Level.INFO, "AI cache: {0} hit(s), {1} miss(es)",
141
                    new Object[] { aiCache.hits(), aiCache.misses() });
142
        }
143
144 2 1. execute : removed conditional - replaced equality check with false → SURVIVED
2. execute : removed conditional - replaced equality check with true → KILLED
        if (secretRunner != null) {
145 1 1. execute : removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::finish → NO_COVERAGE
            secretRunner.finish(roots, null);
146
        }
147
148 2 1. execute : removed conditional - replaced equality check with true → KILLED
2. execute : removed conditional - replaced equality check with false → KILLED
        return hadErrors ? 1 : 0;
149
    }
150
}

Mutations

87

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppContentHashTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppContentHashTest]/[method:csvMode_headerIncludesContentHashColumn_whenEnabled(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:emitMetadata_aiEnabled_emitsBuiltinDefaultTaxonomy(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

88

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppManualTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppManualTest]/[method:manualConsume_emitsEmptyAiColumnsForClassesWithoutResponseFile(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppJsonTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppJsonTest]/[method:minConfidence_filtersLowConfidenceMethods(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

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

4.4
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppJsonTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppJsonTest]/[method:minConfidence_filtersLowConfidenceMethods(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

90

1.1
Location : execute
Killed by : none
removed conditional - replaced equality check with true → TIMED_OUT

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

95

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:emitMetadata_aiEnabled_emitsBuiltinDefaultTaxonomy(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_noPackageDeclaration_usesSimpleClassNameAsFqcn(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

98

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:emitMetadata_aiEnabled_emitsBuiltinDefaultTaxonomy(java.nio.file.Path)]
removed call to org/egothor/methodatlas/emit/OutputEmitter::emitMetadata → KILLED

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

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

101

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_noPackageDeclaration_usesSimpleClassNameAsFqcn(java.nio.file.Path)]
removed call to org/egothor/methodatlas/emit/OutputEmitter::emitCsvHeader → KILLED

109

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppAiTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppAiTest]/[method:plainMode_aiFailureForOneClass_continuesScanningAndFallsBackForThatClass(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

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

112

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

2.2
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_unparseableFile_returnsExitCode1(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

121

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

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

123

1.1
Location : lambda$execute$0
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_noPackageDeclaration_usesSimpleClassNameAsFqcn(java.nio.file.Path)]
removed call to org/egothor/methodatlas/emit/OutputEmitter::emit → KILLED

124

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_unparseableFile_returnsExitCode1(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppCoverageTest]/[method:emitCoverage_writesReportToCustomPath(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

132

1.1
Location : execute
Killed by : none
removed call to org/egothor/methodatlas/command/PluginLoader::closeAll → SURVIVED
Covering tests

137

1.1
Location : execute
Killed by : none
removed call to org/egothor/methodatlas/emit/OutputEmitter::finish → SURVIVED
Covering tests

144

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

2.2
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_unparseableFile_returnsExitCode1(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

145

1.1
Location : execute
Killed by : none
removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::finish → NO_COVERAGE

148

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppCoverageTest]/[method:emitCoverage_writesReportToCustomPath(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_unparseableFile_returnsExitCode1(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1