diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index dd32e14e70..0e7842efcc 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,10 +1,13 @@ name: Java CI -on: [push] +on: + - pull_request + - push + branches: + - master jobs: build: - runs-on: ubuntu-latest steps: diff --git a/pom.xml b/pom.xml index 0bac52df1b..eee04b59f3 100644 --- a/pom.xml +++ b/pom.xml @@ -64,10 +64,6 @@ 1.13 1.24 3.9.1 - 2.34 - 2.67 - 2.19 - 2.32 1.19 1.5 2.9.4 @@ -124,9 +120,11 @@ - org.jenkins-ci.plugins - credentials - 2.1.16 + io.jenkins.tools.bom + bom-2.138.x + 3 + import + pom com.google.code.gson @@ -148,16 +146,6 @@ mailer 1.20 - - org.jenkins-ci.plugins - scm-api - 2.2.7 - - - org.jenkins-ci.plugins.workflow - workflow-scm-step - 2.6 - commons-net commons-net @@ -168,17 +156,16 @@ commons-lang3 ${commons.lang.version} - - org.jenkins-ci.plugins - ssh-credentials - 1.13 - org.slf4j slf4j-api ${slf4j.version} - + + org.jenkins-ci.plugins + scm-api + 2.6.3 + @@ -289,32 +276,26 @@ org.jenkins-ci.plugins script-security - ${script-security.version} org.jenkins-ci.plugins.workflow workflow-api - ${workflow-api.version} org.jenkins-ci.plugins.workflow workflow-cps - ${workflow-cps.version} org.jenkins-ci.plugins.workflow workflow-step-api - ${workflow-step.version} org.jenkins-ci.plugins.workflow workflow-job - ${workflow-job.version} org.jenkins-ci.plugins structs - ${structs.version} org.jenkins-ci.plugins @@ -326,12 +307,10 @@ org.jenkins-ci.plugins credentials - ${credentials.version} org.jenkins-ci.plugins apache-httpcomponents-client-4-api - ${httpcomponents-client.version} @@ -519,7 +498,6 @@ org.jenkins-ci.plugins cloudbees-folder - ${folder-plugin.version} test @@ -534,14 +512,12 @@ org.jenkins-ci.plugins.workflow workflow-durable-task-step - 2.15 test org.jenkins-ci.plugins.workflow workflow-basic-steps - 2.7 test @@ -575,7 +551,7 @@ org.jenkins-ci.plugins scm-api - 2.2.7 + 2.6.3 test-jar test @@ -584,7 +560,6 @@ org.jenkins-ci.plugins.workflow workflow-support - 3.2 test diff --git a/src/main/java/io/jenkins/plugins/analysis/core/model/ReportScanningTool.java b/src/main/java/io/jenkins/plugins/analysis/core/model/ReportScanningTool.java index d7a1a46c05..23e4b59815 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/model/ReportScanningTool.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/model/ReportScanningTool.java @@ -8,6 +8,7 @@ import edu.hm.hafner.analysis.IssueParser; import edu.hm.hafner.analysis.ParsingCanceledException; import edu.hm.hafner.analysis.ParsingException; +import edu.hm.hafner.analysis.ReaderFactory; import edu.hm.hafner.analysis.Report; import edu.hm.hafner.util.Ensure; import edu.umd.cs.findbugs.annotations.Nullable; @@ -117,9 +118,15 @@ public String getReportEncoding() { @Override public Report scan(final Run run, final FilePath workspace, final Charset sourceCodeEncoding, final LogHandler logger) { + return scan(run, workspace, sourceCodeEncoding, logger, new ConsoleLogReaderFactory(run)); + } + + @Override + public Report scan(final Run run, final FilePath workspace, final Charset sourceCodeEncoding, + final LogHandler logger, final ReaderFactory readerFactory) { String actualPattern = getActualPattern(); if (StringUtils.isBlank(actualPattern)) { - return scanInConsoleLog(workspace, run, logger); + return scanInConsoleLog(workspace, run, logger, readerFactory); } else { if (StringUtils.isBlank(getPattern())) { @@ -159,7 +166,8 @@ private Report scanInWorkspace(final FilePath workspace, final String expandedPa } } - private Report scanInConsoleLog(final FilePath workspace, final Run run, final LogHandler logger) { + private Report scanInConsoleLog(final FilePath workspace, final Run run, final LogHandler logger, + final ReaderFactory readerFactory) { Ensure.that(getDescriptor().canScanConsoleLog()).isTrue( "Static analysis tool %s cannot scan console log output, please define a file pattern", getActualName()); @@ -170,7 +178,7 @@ private Report scanInConsoleLog(final FilePath workspace, final Run run, f consoleReport.logInfo("Parsing console log (workspace: '%s')", workspace); logger.log(consoleReport); - Report report = createParser().parse(new ConsoleLogReaderFactory(run)); + Report report = createParser().parse(readerFactory); if (getDescriptor().isConsoleLog()) { report.stream().filter(issue -> !issue.hasFileName()) diff --git a/src/main/java/io/jenkins/plugins/analysis/core/model/Tool.java b/src/main/java/io/jenkins/plugins/analysis/core/model/Tool.java index f5852bdb84..80d5dca0ab 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/model/Tool.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/model/Tool.java @@ -7,6 +7,7 @@ import edu.hm.hafner.analysis.ParsingCanceledException; import edu.hm.hafner.analysis.ParsingException; +import edu.hm.hafner.analysis.ReaderFactory; import edu.hm.hafner.analysis.Report; import org.kohsuke.stapler.DataBoundSetter; @@ -139,6 +140,31 @@ public ToolDescriptor getDescriptor() { public abstract Report scan(Run run, FilePath workspace, Charset sourceCodeEncoding, LogHandler logger) throws ParsingException, ParsingCanceledException; + /** + * Scans the results of a build for issues. This method is invoked on Jenkins master. I.e., if a tool wants to + * process some build results it is required to run a {@link MasterToSlaveCallable}. + * + * @param run + * the build + * @param workspace + * the workspace of the build + * @param sourceCodeEncoding + * the encoding to use to read source files + * @param logger + * the logger + * + * @return the created report + * @throws ParsingException + * Signals that during parsing a non recoverable error has been occurred + * @throws ParsingCanceledException + * Signals that the parsing has been aborted by the user + */ + public Report scan(final Run run, final FilePath workspace, final Charset sourceCodeEncoding, final LogHandler logger, + final ReaderFactory factory) + throws ParsingException, ParsingCanceledException { + return scan(run, workspace, sourceCodeEncoding, logger); + } + /** Descriptor for {@link Tool}. **/ public abstract static class ToolDescriptor extends Descriptor { private final String id; diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/AnalysisExecution.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/AnalysisExecution.java index a5bfddadb0..c11ab62e4a 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/steps/AnalysisExecution.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/AnalysisExecution.java @@ -1,18 +1,34 @@ package io.jenkins.plugins.analysis.core.steps; import java.io.IOException; +import java.io.OutputStream; +import java.io.Reader; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; +import edu.hm.hafner.analysis.ParsingException; +import edu.hm.hafner.analysis.ReaderFactory; +import edu.umd.cs.findbugs.annotations.NonNull; + +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.log.TaskListenerDecorator; +import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import hudson.FilePath; +import hudson.console.ConsoleLogFilter; import hudson.model.Computer; import hudson.model.Run; import hudson.model.TaskListener; +import hudson.remoting.TeeOutputStream; import hudson.remoting.VirtualChannel; +import io.jenkins.plugins.analysis.core.model.Tool; import io.jenkins.plugins.analysis.core.util.ModelValidation; +import io.jenkins.plugins.analysis.core.util.PipelineResultHandler; /** * Base class for static analysis step executions. Provides several helper methods to obtain the defined {@link @@ -30,15 +46,6 @@ abstract class AnalysisExecution extends SynchronousNonBlockingStepExecution< super(context); } - /** - * Returns the associated pipeline run. - * - * @return the run - * @throws IOException - * if the run could be be resolved - * @throws InterruptedException - * if the user canceled the run - */ protected Run getRun() throws IOException, InterruptedException { Run run = getContext().get(Run.class); @@ -48,16 +55,17 @@ abstract class AnalysisExecution extends SynchronousNonBlockingStepExecution< return run; } + + protected FlowNode getFlowNode() throws IOException, InterruptedException { + FlowNode flowNode = getContext().get(FlowNode.class); + + if (flowNode == null) { + throw new IOException("Can't resolve FlowNode for " + toString()); + } + + return flowNode; + } - /** - * Returns a {@link VirtualChannel} to the agent where this step has been executed. - * - * @return the channel - * @throws IOException - * if the computer could be be resolved - * @throws InterruptedException - * if the user canceled the run - */ protected Optional getChannel() throws IOException, InterruptedException { Computer computer = getContext().get(Computer.class); @@ -68,28 +76,10 @@ protected Optional getChannel() throws IOException, InterruptedE return Optional.ofNullable(computer.getChannel()); } - /** - * Returns Jenkins' build folder. - * - * @return the build folder - * @throws IOException - * if the build folder could be be resolved - * @throws InterruptedException - * if the user canceled the run - */ protected FilePath getBuildFolder() throws IOException, InterruptedException { return new FilePath(getRun().getRootDir()); } - /** - * Returns the workspace for this job. - * - * @return the workspace - * @throws IOException - * if the workspace could not be resolved - * @throws InterruptedException - * if the user canceled the execution - */ protected FilePath getWorkspace() throws IOException, InterruptedException { FilePath workspace = getContext().get(FilePath.class); @@ -97,16 +87,11 @@ protected FilePath getWorkspace() throws IOException, InterruptedException { throw new IOException("No workspace available for " + toString()); } + workspace.mkdirs(); + return workspace; } - /** - * Returns the {@link TaskListener} for this execution. - * - * @return the task listener (or a silent listener if no task listener could be found) - * @throws InterruptedException - * if the user canceled the execution - */ protected TaskListener getTaskListener() throws InterruptedException { try { TaskListener listener = getContext().get(TaskListener.class); @@ -120,17 +105,121 @@ protected TaskListener getTaskListener() throws InterruptedException { return TaskListener.NULL; } - /** - * Returns the default charset for the specified encoding string. If the default encoding is empty or {@code null}, - * or if the charset is not valid then the default encoding of the platform is returned. - * - * @param charset - * identifier of the character set - * - * @return the default charset for the specified encoding string - */ protected Charset getCharset(final String charset) { return new ModelValidation().getCharset(charset); } + /** + * Splits off a second branch of the console log into a temporary file that can be parsed by a {@link Tool} parser + * later on. + */ + static class LogSplitter extends TaskListenerDecorator { + private static final long serialVersionUID = -4867121027779734489L; + + private final String log; + + LogSplitter(final String fileName) { + log = fileName; + } + + @Override @NonNull + public OutputStream decorate(@NonNull final OutputStream logger) throws IOException { + return new TeeOutputStream(logger, Files.newOutputStream(Paths.get(log))); + } + } + + /** + * Splits off a second branch of the console log into a temporary file that can be parsed by a {@link Tool} parser + * later on. + */ + static class ConsoleLogSplitter extends ConsoleLogFilter { + private static final long serialVersionUID = -4867121027779734489L; + + private final String log; + + ConsoleLogSplitter(final String fileName) { + log = fileName; + } + + @Override + public OutputStream decorateLogger(final Run build, final OutputStream logger) + throws IOException, InterruptedException { + return super.decorateLogger(build, logger); + } + } + + /** + * Callback that runs after the body of this step has been executed. This callback will record the issues of the + * console log of this block. + */ + static class RecordIssuesCallback extends BodyExecutionCallback { + private static final long serialVersionUID = -2269253566145222283L; + + private final IssuesRecorder recorder; + private final String consoleLogFileName; + + RecordIssuesCallback(final IssuesRecorder recorder, + final String consoleLogFileName) { + this.recorder = recorder; + this.consoleLogFileName = consoleLogFileName; + } + + @Override + public void onSuccess(final StepContext context, final Object result) { + ContextFacade contextFacade = new ContextFacade(context); + BlockOutputReaderFactory readerFactory = new BlockOutputReaderFactory(Paths.get(consoleLogFileName), + contextFacade.getCharset()); + System.out.println("=================="); + readerFactory.readStream().forEach(line -> System.out.format(">>> %s\n", line)); + System.out.println("=================="); + RecordIssuesRunner runner = new RecordIssuesRunner(); + runner.run(recorder, contextFacade, readerFactory); + } + + @Override + public void onFailure(final StepContext context, final Throwable t) { + // silently ignore + } + } + + static class RecordIssuesRunner { + void run(final IssuesRecorder recorder, final ContextFacade context, final ReaderFactory readerFactory) { + try { + recorder.perform(context.getRun(), context.getWorkspace(), context.getTaskListener(), + new PipelineResultHandler(context.getRun(), + context.getFlowNode()), readerFactory); + } + catch (IOException | InterruptedException exception) { + // silently ignore + } + } + } + + /** + * Provides a reader factory for the portion of the console log of a script block. + */ + private static class BlockOutputReaderFactory extends ReaderFactory { + private final Path log; + + BlockOutputReaderFactory(final Path log, final Charset charset) { + super(charset); + + this.log = log; + } + + @Override + public String getFileName() { + return "block-console.log"; + } + + @Override + public Reader create() { + try { + return Files.newBufferedReader(log); + } + catch (IOException e) { + throw new ParsingException(e); + } + } + } } diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/ContextFacade.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/ContextFacade.java new file mode 100644 index 0000000000..af4fd8edde --- /dev/null +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/ContextFacade.java @@ -0,0 +1,151 @@ +package io.jenkins.plugins.analysis.core.steps; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import hudson.FilePath; +import hudson.model.Computer; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.remoting.VirtualChannel; + +/** + * Provides access to all required instances on the {@link StepContext}. + * + * @author Ullrich Hafner + */ +public class ContextFacade { + private final StepContext context; + + ContextFacade(final StepContext context) { + this.context = context; + } + + StepContext getContext() { + return context; + } + + /** + * Returns the associated pipeline run. + * + * @return the run + * @throws IOException + * if the run could be be resolved + * @throws InterruptedException + * if the user canceled the build + */ + protected Run getRun() throws IOException, InterruptedException { + Run run = getContext().get(Run.class); + + if (run == null) { + throw new IOException("Can't resolve Run for " + toString()); + } + + return run; + } + + /** + * Returns the associated pipeline {@link FlowNode}. + * + * @return the flow node + * @throws IOException + * if the flow node could be be resolved + * @throws InterruptedException + * if the user canceled the build + */ + protected FlowNode getFlowNode() throws IOException, InterruptedException { + FlowNode flowNode = getContext().get(FlowNode.class); + + if (flowNode == null) { + throw new IOException("Can't resolve FlowNode for " + toString()); + } + + return flowNode; + } + + /** + * Returns a {@link VirtualChannel} to the agent where this step has been executed. + * + * @return the channel + * @throws IOException + * if the computer could be be resolved + * @throws InterruptedException + * if the user canceled the build + */ + protected Optional getChannel() throws IOException, InterruptedException { + Computer computer = getContext().get(Computer.class); + + if (computer == null) { + return Optional.empty(); + } + + return Optional.ofNullable(computer.getChannel()); + } + + /** + * Returns Jenkins' build folder. + * + * @return the build folder + * @throws IOException + * if the build folder could be be resolved + * @throws InterruptedException + * if the user canceled the build + */ + protected FilePath getBuildFolder() throws IOException, InterruptedException { + return new FilePath(getRun().getRootDir()); + } + + /** + * Returns the workspace for this job. + * + * @return the workspace + * @throws IOException + * if the workspace could not be resolved + * @throws InterruptedException + * if the user canceled the execution + */ + protected FilePath getWorkspace() throws IOException, InterruptedException { + FilePath workspace = getContext().get(FilePath.class); + + if (workspace == null) { + throw new IOException("No workspace available for " + toString()); + } + + workspace.mkdirs(); + + return workspace; + } + + /** + * Returns the {@link TaskListener} for this execution. + * + * @return the task listener (or a silent listener if no task listener could be found) + * @throws InterruptedException + * if the user canceled the execution + */ + protected TaskListener getTaskListener() throws InterruptedException { + try { + TaskListener listener = getContext().get(TaskListener.class); + if (listener != null) { + return listener; + } + } + catch (IOException ignored) { + // ignore + } + return TaskListener.NULL; + } + + Charset getCharset() { + try { + return getRun().getCharset(); + } + catch (IOException | InterruptedException ignored) { + return StandardCharsets.UTF_8; + } + } +} diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java index e331d9e6f4..ee56799454 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java @@ -2,6 +2,7 @@ package io.jenkins.plugins.analysis.core.steps; import java.io.IOException; +import java.io.Serializable; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; @@ -10,6 +11,7 @@ import org.apache.commons.lang3.StringUtils; +import edu.hm.hafner.analysis.ReaderFactory; import edu.hm.hafner.analysis.Severity; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -46,6 +48,7 @@ import io.jenkins.plugins.analysis.core.model.Tool; import io.jenkins.plugins.analysis.core.steps.IssuesScanner.BlameMode; import io.jenkins.plugins.analysis.core.steps.IssuesScanner.ForensicsMode; +import io.jenkins.plugins.analysis.core.util.ConsoleLogReaderFactory; import io.jenkins.plugins.analysis.core.util.HealthDescriptor; import io.jenkins.plugins.analysis.core.util.LogHandler; import io.jenkins.plugins.analysis.core.util.ModelValidation; @@ -75,7 +78,9 @@ * @author Ullrich Hafner */ @SuppressWarnings({"PMD.ExcessivePublicCount", "PMD.ExcessiveClassLength", "PMD.ExcessiveImports", "PMD.TooManyFields", "PMD.DataClass", "ClassDataAbstractionCoupling", "ClassFanOutComplexity"}) -public class IssuesRecorder extends Recorder { +public class IssuesRecorder extends Recorder implements Serializable { + private static final long serialVersionUID = -5129133484590854697L; + static final String NO_REFERENCE_JOB = "-"; static final String DEFAULT_ID = "analysis"; @@ -524,7 +529,7 @@ public boolean perform(final AbstractBuild build, final Launcher launcher, throw new IOException("No workspace found for " + build); } - perform(build, workspace, listener, new RunResultHandler(build)); + perform(build, workspace, listener, new RunResultHandler(build), new ConsoleLogReaderFactory(build)); return true; } @@ -534,10 +539,10 @@ public boolean perform(final AbstractBuild build, final Launcher launcher, * Pipeline-specific behavior. */ void perform(final Run run, final FilePath workspace, final TaskListener listener, - final StageResultHandler statusHandler) throws InterruptedException, IOException { + final StageResultHandler statusHandler, final ReaderFactory readerFactory) throws InterruptedException, IOException { Result overallResult = run.getResult(); if (isEnabledForFailure || overallResult == null || overallResult.isBetterOrEqualTo(Result.UNSTABLE)) { - record(run, workspace, listener, statusHandler); + record(run, workspace, listener, statusHandler, readerFactory); } else { LogHandler logHandler = new LogHandler(listener, createLoggerPrefix()); @@ -550,12 +555,12 @@ private String createLoggerPrefix() { } private void record(final Run run, final FilePath workspace, final TaskListener listener, - final StageResultHandler statusHandler) + final StageResultHandler statusHandler, final ReaderFactory readerFactory) throws IOException, InterruptedException { if (isAggregatingResults && analysisTools.size() > 1) { AnnotatedReport totalIssues = new AnnotatedReport(StringUtils.defaultIfEmpty(id, DEFAULT_ID)); for (Tool tool : analysisTools) { - totalIssues.add(scanWithTool(run, workspace, listener, tool), tool.getActualId()); + totalIssues.add(scanWithTool(run, workspace, listener, tool, readerFactory), tool.getActualId()); } String toolName = StringUtils.defaultIfEmpty(getName(), Messages.Tool_Default_Name()); publishResult(run, listener, toolName, totalIssues, toolName, statusHandler); @@ -567,7 +572,7 @@ private void record(final Run run, final FilePath workspace, final TaskLis report.logInfo("Ignoring 'aggregatingResults' and ID '%s' since only a single tool is defined.", id); } - report.add(scanWithTool(run, workspace, listener, tool)); + report.add(scanWithTool(run, workspace, listener, tool, readerFactory)); if (StringUtils.isNotBlank(id) || StringUtils.isNotBlank(name)) { report.logInfo("Ignoring name='%s' and id='%s' when publishing non-aggregating reports", name, id); @@ -596,10 +601,10 @@ private String getReportName(final Tool tool) { } private AnnotatedReport scanWithTool(final Run run, final FilePath workspace, final TaskListener listener, - final Tool tool) throws IOException, InterruptedException { + final Tool tool, final ReaderFactory readerFactory) throws IOException, InterruptedException { IssuesScanner issuesScanner = new IssuesScanner(tool, getFilters(), getSourceCodeCharset(), workspace, run, new FilePath(run.getRootDir()), listener, isBlameDisabled ? BlameMode.DISABLED : BlameMode.ENABLED, - isForensicsDisabled ? ForensicsMode.DISABLED : ForensicsMode.ENABLED); + isForensicsDisabled ? ForensicsMode.DISABLED : ForensicsMode.ENABLED, readerFactory); return issuesScanner.scan(); } diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesScanner.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesScanner.java index 0a4f0c2a75..962a0733e1 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesScanner.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesScanner.java @@ -20,6 +20,7 @@ import edu.hm.hafner.analysis.ModuleDetector.FileSystem; import edu.hm.hafner.analysis.ModuleResolver; import edu.hm.hafner.analysis.PackageNameResolver; +import edu.hm.hafner.analysis.ReaderFactory; import edu.hm.hafner.analysis.Report; import edu.hm.hafner.analysis.Report.IssueFilterBuilder; @@ -66,6 +67,7 @@ class IssuesScanner { private final TaskListener listener; private final BlameMode blameMode; private final ForensicsMode forensicsMode; + private final ReaderFactory readerFactory; enum BlameMode { ENABLED, DISABLED @@ -78,7 +80,8 @@ enum ForensicsMode { @SuppressWarnings("checkstyle:ParameterNumber") IssuesScanner(final Tool tool, final List filters, final Charset sourceCodeEncoding, final FilePath workspace, final Run run, final FilePath jenkinsRootDir, final TaskListener listener, - final BlameMode blameMode, final ForensicsMode forensicsMode) { + final BlameMode blameMode, final ForensicsMode forensicsMode, + final ReaderFactory readerFactory) { this.filters = new ArrayList<>(filters); this.sourceCodeEncoding = sourceCodeEncoding; this.tool = tool; @@ -88,11 +91,12 @@ enum ForensicsMode { this.listener = listener; this.blameMode = blameMode; this.forensicsMode = forensicsMode; + this.readerFactory = readerFactory; } public AnnotatedReport scan() throws IOException, InterruptedException { LogHandler logger = new LogHandler(listener, tool.getActualName()); - Report report = tool.scan(run, workspace, sourceCodeEncoding, logger); + Report report = tool.scan(run, workspace, sourceCodeEncoding, logger, readerFactory); if (tool.getDescriptor().isPostProcessingEnabled()) { return postProcess(report, logger); diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java index 5bddf249ef..a2faa5e119 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java @@ -2,6 +2,8 @@ import java.io.IOException; import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -33,12 +35,11 @@ import io.jenkins.plugins.analysis.core.model.ResultAction; import io.jenkins.plugins.analysis.core.model.StaticAnalysisLabelProvider; import io.jenkins.plugins.analysis.core.model.Tool; -import io.jenkins.plugins.analysis.core.util.PipelineResultHandler; +import io.jenkins.plugins.analysis.core.util.ConsoleLogReaderFactory; import io.jenkins.plugins.analysis.core.util.QualityGate; import io.jenkins.plugins.analysis.core.util.QualityGate.QualityGateResult; import io.jenkins.plugins.analysis.core.util.QualityGate.QualityGateType; import io.jenkins.plugins.analysis.core.util.QualityGateEvaluator; -import io.jenkins.plugins.analysis.core.util.StageResultHandler; import io.jenkins.plugins.analysis.core.util.TrendChartType; /** @@ -923,6 +924,7 @@ static class Execution extends AnalysisExecution { Execution(@NonNull final StepContext context, final RecordIssuesStep step) { super(context); + this.step = step; } @@ -948,15 +950,23 @@ protected Void run() throws IOException, InterruptedException { recorder.setFailOnError(step.getFailOnError()); recorder.setTrendChartType(step.trendChartType); - StageResultHandler statusHandler = new PipelineResultHandler(getRun(), - getContext().get(FlowNode.class)); + if (getContext().hasBody()) { + Path blockLog = Files.createTempFile("warnings-ng", "console-log"); + + getContext().newBodyInvoker() +// .withContext(new LogSplitter(blockLog.toString())) + .withContext(new ConsoleLogSplitter(blockLog.toString())) + .withCallback(new RecordIssuesCallback(recorder, blockLog.toString())) + .start(); + } + else { + RecordIssuesRunner runner = new RecordIssuesRunner(); + + runner.run(recorder, new ContextFacade(getContext()), new ConsoleLogReaderFactory(getRun())); + } - FilePath workspace = getWorkspace(); - workspace.mkdirs(); - recorder.perform(getRun(), workspace, getTaskListener(), statusHandler); return null; } - } /** @@ -980,5 +990,10 @@ public String getDisplayName() { public Set> getRequiredContext() { return Sets.immutable.of(FilePath.class, FlowNode.class, Run.class, TaskListener.class).castToSet(); } + + @Override + public boolean takesImplicitBlockArgument() { + return true; + } } } diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/ScanForIssuesStep.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/ScanForIssuesStep.java index e777a9a167..678128c162 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/steps/ScanForIssuesStep.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/ScanForIssuesStep.java @@ -26,6 +26,7 @@ import io.jenkins.plugins.analysis.core.model.Tool; import io.jenkins.plugins.analysis.core.steps.IssuesScanner.BlameMode; import io.jenkins.plugins.analysis.core.steps.IssuesScanner.ForensicsMode; +import io.jenkins.plugins.analysis.core.util.ConsoleLogReaderFactory; /** * Scan files or the console log for issues. @@ -164,7 +165,7 @@ protected AnnotatedReport run() throws IOException, InterruptedException, Illega IssuesScanner issuesScanner = new IssuesScanner(tool, filters, getCharset(sourceCodeEncoding), workspace, getRun(), new FilePath(getRun().getRootDir()), listener, isBlameDisabled ? BlameMode.DISABLED : BlameMode.ENABLED, - isForensicsDisabled ? ForensicsMode.DISABLED : ForensicsMode.ENABLED); + isForensicsDisabled ? ForensicsMode.DISABLED : ForensicsMode.ENABLED, new ConsoleLogReaderFactory(getRun())); return issuesScanner.scan(); } diff --git a/src/test/java/io/jenkins/plugins/analysis/core/testutil/IntegrationTest.java b/src/test/java/io/jenkins/plugins/analysis/core/testutil/IntegrationTest.java index 779ebad532..b83fe1523a 100644 --- a/src/test/java/io/jenkins/plugins/analysis/core/testutil/IntegrationTest.java +++ b/src/test/java/io/jenkins/plugins/analysis/core/testutil/IntegrationTest.java @@ -848,7 +848,7 @@ protected AnalysisResult scheduleSuccessfulBuild(final ParameterizedJob jo return action.getResult(); } - private void logConsole(final Run run) { + protected void logConsole(final Run run) { try (Reader reader = run.getLogReader()) { try (BufferedReader bufferedReader = new BufferedReader(reader)) { bufferedReader.lines().forEach(System.out::println); diff --git a/src/test/java/io/jenkins/plugins/analysis/warnings/StepsITest.java b/src/test/java/io/jenkins/plugins/analysis/warnings/StepsITest.java index 16b46438f4..b7da6d1d30 100644 --- a/src/test/java/io/jenkins/plugins/analysis/warnings/StepsITest.java +++ b/src/test/java/io/jenkins/plugins/analysis/warnings/StepsITest.java @@ -51,6 +51,29 @@ */ @SuppressWarnings({"PMD.ExcessiveImports", "checkstyle:ClassDataAbstractionCoupling", "checkstyle:ClassFanOutComplexity"}) public class StepsITest extends IntegrationTestWithJenkinsPerTest { + @Test @org.jvnet.hudson.test.Issue("JENKINS-44450") + public void shouldRunClosure() { + createAgentWithEnabledSecurity("agent"); + + WorkflowJob job = createPipeline(); + job.setDefinition(new CpsFlowDefinition("node ('agent') {\n" + + " stage ('Block Scoped Usage') {\n" + + " echo 'MediaPortal.cs(1,1): warning CS0162: Not recorded'\n" + + " recordIssues (tools: [msBuild(), java()], aggregatingResults: true) {" + + " echo 'MediaPortal.cs(2,1): warning CS0162: Recorded'\n" + + " sleep (time: 5, unit: 'SECONDS')\n" + + " echo 'MediaPortal.cs(3,1): warning CS0162: Recorded'\n" + + " } \n" + + " echo 'MediaPortal.cs(4,1): warning CS0162: Not recorded'\n" + + " } \n" + + "}", true)); + + AnalysisResult result = scheduleSuccessfulBuild(job); + + assertThat(result).hasTotalSize(1); + assertThat(result.getIssues().get(0)).hasFileName("MediaPortal.cs").hasLineStart(2); + } + /** Verifies that a {@link Tool} defines a {@link Symbol}. */ @Test public void shouldProvideSymbol() {