Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

408 analyze should support a baseline for existing violations warnings and failures #477

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5fdc4b5
#408 added initial implementation for BaselineManager
DirkMahler Jul 10, 2024
fa12168
Merge branch 'refs/heads/master' into 408-analyze-should-support-a-ba…
DirkMahler Jul 11, 2024
ecbabbe
#408 first integration of BaselineManager into AnalyzerImpl
DirkMahler Jul 11, 2024
a9dc709
#408 allow to enable/disable of BaselineManager
DirkMahler Jul 11, 2024
8a4be8e
Merge branch 'refs/heads/master' into 408-analyze-should-support-a-ba…
DirkMahler Jul 12, 2024
7ae5449
#408 added initial integration of BaselineManager with AnalyzerContext
DirkMahler Jul 12, 2024
85e8729
#408 minor change to AnalyzerContextImpl regarding suppressions
DirkMahler Jul 12, 2024
5907f2d
#408 fixed failing unit test
DirkMahler Jul 12, 2024
b6f1e3d
Merge branch 'refs/heads/master' into 408-analyze-should-support-a-ba…
DirkMahler Jul 13, 2024
e1804ac
#408 added rule filters to BaselineManager
DirkMahler Jul 13, 2024
f459788
#408 added baseline IT for Maven and updated manual
DirkMahler Jul 13, 2024
1e120ba
#408 fixed BaselineRepositoryTest
DirkMahler Jul 14, 2024
d6c2767
#408 update baseline file only if the baseline actually changed
DirkMahler Jul 14, 2024
b166785
#408 fixed log message
DirkMahler Jul 14, 2024
8f89caa
#408 added baseline test to AnalyzeIT
DirkMahler Jul 14, 2024
a82a8af
#408 minor cleanups in BaselineRepository
DirkMahler Jul 14, 2024
9e6cdeb
Merge branch 'refs/heads/master' into 408-analyze-should-support-a-ba…
DirkMahler Jul 19, 2024
d0859db
#477 fixed PR review comments and uodated from master
DirkMahler Jul 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,8 @@ protected boolean isConnectorRequired() {

protected RuleSet getAvailableRules(Rule rule) throws CliExecutionException {
List<RuleSource> sources = new ArrayList<>();
File selectedDirectory = new File(rule.directory()
.orElse(DEFAULT_RULE_DIRECTORY));
// read rules from rules directory
sources.addAll(readRulesDirectory(selectedDirectory));
sources.addAll(readRulesDirectory(rule));
List<RuleSource> ruleSources = pluginRepository.getRulePluginRepository()
.getRuleSources();
sources.addAll(ruleSources);
Expand All @@ -51,6 +49,18 @@ protected RuleSet getAvailableRules(Rule rule) throws CliExecutionException {
}
}

/**
* Determines the directory containing rules.
*
* @param rule
* The {@link Rule} configuration.
* @return The rules directory.
*/
protected static File getRulesDirectory(Rule rule) {
return new File(rule.directory()
.orElse(DEFAULT_RULE_DIRECTORY));
}

/**
* Return the selection of rules.
*
Expand All @@ -64,7 +74,8 @@ protected RuleSelection getRuleSelection(RuleSet ruleSet, Analyze analyze) {
return RuleSelection.select(ruleSet, analyze.groups(), analyze.constraints(), analyze.excludeConstraints(), analyze.concepts());
}

private List<RuleSource> readRulesDirectory(File rulesDirectory) throws CliExecutionException {
private List<RuleSource> readRulesDirectory(Rule rule) throws CliExecutionException {
File rulesDirectory = getRulesDirectory(rule);
if (rulesDirectory.exists() && !rulesDirectory.isDirectory()) {
throw new CliExecutionException(rulesDirectory.getAbsolutePath() + " does not exist or is not a directory.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
import com.buschmais.jqassistant.commandline.CliRuleViolationException;
import com.buschmais.jqassistant.commandline.configuration.CliConfiguration;
import com.buschmais.jqassistant.core.analysis.api.Analyzer;
import com.buschmais.jqassistant.core.analysis.api.baseline.BaselineManager;
import com.buschmais.jqassistant.core.analysis.api.baseline.BaselineRepository;
import com.buschmais.jqassistant.core.analysis.api.configuration.Analyze;
import com.buschmais.jqassistant.core.analysis.api.configuration.Baseline;
import com.buschmais.jqassistant.core.analysis.impl.AnalyzerImpl;
import com.buschmais.jqassistant.core.analysis.spi.AnalyzerPluginRepository;
import com.buschmais.jqassistant.core.report.api.ReportContext;
Expand Down Expand Up @@ -49,8 +52,11 @@ public void run(CliConfiguration configuration, Options options) throws CliExecu
.report(), reportContext);
InMemoryReportPlugin inMemoryReportPlugin = new InMemoryReportPlugin(new CompositeReportPlugin(reportPlugins));
try {
Baseline baselineConfiguration = analyze.baseline();
BaselineRepository baselineRepository = new BaselineRepository(baselineConfiguration, getRulesDirectory(analyze.rule()));
BaselineManager baselineManager = new BaselineManager(baselineConfiguration, baselineRepository);
Analyzer analyzer = new AnalyzerImpl(analyze, pluginRepository.getClassLoader(), store, pluginRepository.getAnalyzerPluginRepository()
.getRuleInterpreterPlugins(emptyMap()), inMemoryReportPlugin);
.getRuleInterpreterPlugins(emptyMap()), baselineManager, inMemoryReportPlugin);
RuleSet availableRules = getAvailableRules(analyze
.rule());
analyzer.execute(availableRules, getRuleSelection(availableRules, analyze));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ public static void beforeAll() {
@BeforeEach
public void before(DISTRIBUTION distribution) throws IOException {
assumeThat(Runtime.version()
.feature()).isGreaterThanOrEqualTo(distribution.minRuntimeVersion.feature())
.isLessThanOrEqualTo(distribution.maxRuntimeVersion.feature());
.feature()).describedAs("Java runtime version")
.isGreaterThanOrEqualTo(distribution.minRuntimeVersion.feature())
.isLessThanOrEqualTo(distribution.maxRuntimeVersion.feature());
this.neo4jVersion = distribution.name()
.toLowerCase(Locale.getDefault());
this.jqaHome = getjQAHomeDirectory(neo4jVersion);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ void constraint() {
withStore(store -> verifyConcepts(store, TEST_CONCEPT));
}

@DistributionTest
void constraintWithBaseline() {
File baselineFile = new File(RULES_DIRECTORY + "/jqassistant-baseline.xml");
if (baselineFile.exists()) {
assertThat(baselineFile.delete()).isTrue();
}
String[] args = new String[] { "analyze", "-D", "jqassistant.analyze.rule.directory=" + RULES_DIRECTORY, "-D",
"jqassistant.analyze.constraints=" + TEST_CONSTRAINT, "-D", "jqassistant.analyze.baseline.enabled=true" };
// create baseline
assertThat(execute(args).getExitCode()).isEqualTo(2);
assertThat(baselineFile).exists();
// create run with baseline
assertThat(execute(args).getExitCode()).isZero();
}

@DistributionTest
void excludeConstraint() {
String[] args = new String[] { "analyze", "-D", "jqassistant.analyze.rule.directory=" + RULES_DIRECTORY, "-D",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.buschmais.jqassistant.core.analysis.api;

import com.buschmais.jqassistant.core.analysis.api.configuration.Analyze;
import com.buschmais.jqassistant.core.rule.api.model.RuleException;
import com.buschmais.jqassistant.core.rule.api.model.RuleSelection;
import com.buschmais.jqassistant.core.rule.api.model.RuleSet;
Expand All @@ -10,13 +9,6 @@
*/
public interface Analyzer {

/**
* Return the {@link Analyze} configuration.
*
* @return The {@link Analyze} configuration.
*/
Analyze getConfiguration();

/**
* Executes the given rule set.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ public interface AnalyzerContext {
*/
Row toRow(ExecutableRule<?> rule, Map<String, Column<?>> columns);

/**
* Verifies if the Row shall be suppressed.
* <p>
* The primary column is checked if it contains a suppression that matches the
* current rule id.
*
* @param executableRule
* The {@link ExecutableRule}.
* @param primaryColumn
* The name of the primary column.
* @param row
* The {@link Row}.
* @return <code>true</code> if the row shall be suppressed.
*/
<T extends ExecutableRule<?>> boolean isSuppressed(T executableRule, String primaryColumn, Row row);

/**
* Verifies the rows returned by a cypher query for an executable.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.buschmais.jqassistant.core.analysis.api.baseline;

import java.util.SortedMap;
import java.util.TreeMap;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;

/**
* Represents a baseline for analyze results.
* <p>
* Note that the data structures rely on {@link SortedMap}s to preserve the order of entries. This is required for creating diffs between baseline files which are generated from these structures.
*/
@Getter
@EqualsAndHashCode
@ToString
public class Baseline {

/**
* The baseline per {@link com.buschmais.jqassistant.core.rule.api.model.Concept} id.
*/
private SortedMap<String, RuleBaseline> concepts = new TreeMap<>();

/**
* The baseline per {@link com.buschmais.jqassistant.core.rule.api.model.Constraint} id.
*/
private SortedMap<String, RuleBaseline> constraints = new TreeMap<>();

/**
* Represent a baseline for a specific {@link com.buschmais.jqassistant.core.rule.api.model.Concept} or {@link com.buschmais.jqassistant.core.rule.api.model.Constraint}.
*/
@Getter
@EqualsAndHashCode
@ToString
public static class RuleBaseline {

/**
* Holds the row key as key and the columns (as human-readable labels) as values.
*/
private final SortedMap<String, SortedMap<String, String>> rows = new TreeMap<>();

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.buschmais.jqassistant.core.analysis.api.baseline;

import java.util.*;
import java.util.function.Function;

import com.buschmais.jqassistant.core.report.api.model.Column;
import com.buschmais.jqassistant.core.report.api.model.Row;
import com.buschmais.jqassistant.core.rule.api.filter.RuleFilter;
import com.buschmais.jqassistant.core.rule.api.model.Concept;
import com.buschmais.jqassistant.core.rule.api.model.Constraint;
import com.buschmais.jqassistant.core.rule.api.model.ExecutableRule;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static java.util.Collections.emptyList;

/**
* Verifies result rows gathered for rule during an analyze task against a baseline
* <p>
* {@link Row}s of a constraint are considered as "new" if
* <ul>
* <li>No baseline exists.</li>
* <li>A baseline exists but does not yet contain the {@link Row}.</li>
* </ul>
* <p>
* Only {@link Row}s that have been validated using {@link #isExisting(ExecutableRule, Row)} ({@link ExecutableRule}, Row)} are copied to the new baseline.
* <p>
*/
@RequiredArgsConstructor
@Slf4j
public class BaselineManager {

private final com.buschmais.jqassistant.core.analysis.api.configuration.Baseline configuration;

private final BaselineRepository baselineRepository;

private Optional<Baseline> optionalOldBaseline;

private Baseline newBaseline;

public void start() {
if (configuration.enabled()) {
optionalOldBaseline = baselineRepository.read();
newBaseline = new Baseline();
}
}

public void stop() {
if (configuration.enabled() && !(optionalOldBaseline.isPresent() && newBaseline.equals(optionalOldBaseline.get()))) {
log.info("Baseline has been updated.");
baselineRepository.write(newBaseline);
}
}

public boolean isExisting(ExecutableRule<?> executableRule, Row row) {
if (!configuration.enabled()) {
return false;
}
if (newBaseline == null) {
throw new IllegalStateException("Baseline manager has not been started yet");
}
if (executableRule instanceof Concept) {
return isExistingResult(executableRule, row, configuration.includeConcepts()
.orElse(emptyList()), Baseline::getConcepts);
} else if (executableRule instanceof Constraint) {
return isExistingResult(executableRule, row, configuration.includeConstraints(), Baseline::getConstraints);
}
throw new IllegalArgumentException("Unsupported executable rule: " + executableRule);
}

private Boolean isExistingResult(ExecutableRule<?> executableRule, Row row, List<String> ruleFilters,
Function<Baseline, SortedMap<String, Baseline.RuleBaseline>> rows) {
String ruleId = executableRule.getId();
if (ruleFilters.stream()
.noneMatch(filter -> RuleFilter.matches(ruleId, filter))) {
return false;
}
String rowKey = row.getKey();
Map<String, Column<?>> columns = row.getColumns();
return optionalOldBaseline.map(oldBaseline -> {
SortedMap<String, Baseline.RuleBaseline> ruleBaseline = rows.apply(oldBaseline);
Baseline.RuleBaseline oldRuleBaseline = ruleBaseline.get(ruleId);
if (oldRuleBaseline != null && oldRuleBaseline.getRows()
.containsKey(rowKey)) {
addToNewBaseline(ruleId, rowKey, columns, rows);
return true;
}
return false;
})
.orElseGet(() -> {
addToNewBaseline(ruleId, rowKey, columns, rows);
return false;
});
}

private void addToNewBaseline(String constraintId, String rowKey, Map<String, Column<?>> columns,
Function<Baseline, SortedMap<String, Baseline.RuleBaseline>> rows) {
Baseline.RuleBaseline newRuleBaseline = rows.apply(newBaseline)
.computeIfAbsent(constraintId, key -> new Baseline.RuleBaseline());
TreeMap<String, String> row = new TreeMap<>();
columns.entrySet()
.stream()
.forEach(entry -> row.put(entry.getKey(), entry.getValue()
.getLabel()));
newRuleBaseline.getRows()
.put(rowKey, row);
}
}
Loading
Loading