├── .gitignore ├── .mvn ├── maven.config └── extensions.xml ├── Jenkinsfile ├── src ├── main │ ├── resources │ │ ├── index.jelly │ │ └── com │ │ │ └── absint │ │ │ └── astree │ │ │ └── AstreeBuilder │ │ │ ├── help-astree_server.html │ │ │ ├── help-skip_analysis.html │ │ │ ├── help-alauncher.html │ │ │ ├── help-dax_file.html │ │ │ ├── help-output_dir.html │ │ │ ├── help-dropAnalysis.html │ │ │ ├── help-analysis_id.html │ │ │ ├── global.jelly │ │ │ ├── help-failonswitch.html │ │ │ └── config.jelly │ └── java │ │ └── com │ │ └── absint │ │ └── astree │ │ ├── AstreeTool.java │ │ ├── AnalysisServerConfiguration.java │ │ ├── StatusPoller.java │ │ ├── FailonSwitch.java │ │ ├── AnalysisSummary.java │ │ ├── AstreeReportParser.java │ │ └── AstreeBuilder.java └── test │ └── java │ └── com │ └── absint │ └── astree │ └── AstreeReportParserTest.java ├── .github ├── dependabot.yml └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── INSTALL ├── LICENSE ├── README.adoc └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /work 3 | /.vscode 4 | *.swp 5 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin( 2 | useContainerAgent: true, 3 | configurations: [ 4 | [platform: 'linux', jdk: 21], 5 | [platform: 'windows', jdk: 17] 6 | ], 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 |
Code:
");
74 | descriptionBuilder.append(code);
75 | descriptionBuilder.append("");
76 | }
77 |
78 | // build description out of context
79 | final String context = message.context;
80 | if (context != null && !context.isEmpty()) {
81 | descriptionBuilder.append("Context:
");
82 | descriptionBuilder.append(context);
83 | descriptionBuilder.append("");
84 | }
85 |
86 | // build category out of message type and category
87 | final AlarmType type = handler.types.get(message.typeID);
88 | if (type == null) {
89 | throw new ParsingException("Missing finding category " + message.typeID);
90 | }
91 | final String category = handler.categories.get(type.categoryID);
92 | if (category == null) {
93 | throw new ParsingException("Missing finding group " + type.categoryID);
94 | }
95 | categoryBuilder.append(type.type);
96 | categoryBuilder.append(" [");
97 | categoryBuilder.append(category);
98 | categoryBuilder.append(']');
99 |
100 | // retrieve location of message
101 | Location location = handler.locations.get(message.locationID);
102 | if (location == null)
103 | location = new Location();
104 |
105 | // create new issue
106 | issueBuilder.setMessage(message.text)
107 | .setFileName(handler.files.get(location.fileID))
108 | .setLineStart(location.startLine)
109 | .setLineEnd(location.endLine)
110 | .setColumnStart(location.startColumn)
111 | .setColumnEnd(location.endColumn)
112 | .setCategory(categoryBuilder.toString())
113 | .setDescription(descriptionBuilder.toString())
114 | .setSeverity(message.severity);
115 |
116 | // add issue to report
117 | report.add(issueBuilder.build());
118 |
119 | descriptionBuilder.setLength(0);
120 | categoryBuilder.setLength(0);
121 | }
122 |
123 | issueBuilder.close();
124 |
125 | return report;
126 | }
127 |
128 | private static class Handler extends DefaultHandler {
129 | public String projectDescription;
130 | public MapContext:
l32#call#main@48,l34#loop@72=11/12", error1.getDescription()); 159 | 160 | final Issue error2 = report.get(24); 161 | assertEquals(fileName, Severity.ERROR, error2.getSeverity()); 162 | assertEquals(fileName, "preprocessed/src/scenarios.c", error2.getFileName()); 163 | assertEquals(fileName, "Definite runtime error [Errors]", error2.getCategory()); 164 | assertEquals(fileName, 78, error2.getLineStart()); 165 | assertEquals(fileName, 8, error2.getColumnStart()); 166 | assertEquals(fileName, 78, error2.getLineEnd()); 167 | assertEquals(fileName, 16, error2.getColumnEnd()); 168 | assertEquals(fileName, "ERROR: Definite runtime error during assignment in this context. Analysis stopped for this context.", error2.getMessage()); 169 | if (fileName.equals("report-18.04.xml")) { 170 | assertEquals(fileName, "n/a", error2.getReference()); 171 | } else { 172 | assertEquals(fileName, "n/A", error2.getReference()); 173 | } 174 | assertEquals(fileName, "
Context:
l32#call#main@48,l37#loop@77=11/12", error2.getDescription()); 175 | 176 | final Issue note1 = report.get(30); 177 | assertEquals(fileName, Severity.WARNING_LOW, note1.getSeverity()); 178 | assertEquals(fileName, "preprocessed/src/scenarios.c", note1.getFileName()); 179 | assertEquals(fileName, "Control flow [Notifications]", note1.getCategory()); 180 | assertEquals(fileName, 48, note1.getLineStart()); 181 | assertEquals(fileName, 1, note1.getColumnStart()); 182 | assertEquals(fileName, 135, note1.getLineEnd()); 183 | assertEquals(fileName, 1, note1.getColumnEnd()); 184 | assertEquals(fileName, "NOTE: Analyzed entry-point main never returns.", note1.getMessage()); 185 | if (fileName.equals("report-18.04.xml")) { 186 | assertEquals(fileName, "n/a", note1.getReference()); 187 | } else { 188 | assertEquals(fileName, "n/A", note1.getReference()); 189 | } 190 | assertEquals(fileName, "", note1.getDescription()); 191 | 192 | final Issue note2 = report.get(35); 193 | assertEquals(fileName, Severity.WARNING_LOW, note2.getSeverity()); 194 | assertEquals(fileName, "preprocessed/src/scenarios.c", note2.getFileName()); 195 | assertEquals(fileName, "Control flow [Notifications]", note2.getCategory()); 196 | assertEquals(fileName, 124, note2.getLineStart()); 197 | assertEquals(fileName, 3, note2.getColumnStart()); 198 | assertEquals(fileName, 126, note2.getLineEnd()); 199 | assertEquals(fileName, 5, note2.getColumnEnd()); 200 | assertEquals(fileName, "NOTE: Loop may be unbounded", note2.getMessage()); 201 | if (fileName.equals("report-18.04.xml")) { 202 | assertEquals(fileName, "n/a", note2.getReference()); 203 | } else { 204 | assertEquals(fileName, "n/A", note2.getReference()); 205 | } 206 | assertEquals(fileName, "", note2.getDescription()); 207 | } else if (fileName.compareTo("report-21.04i2.xml") <= 0) { 208 | assertEquals(fileName, 125, report.size()); 209 | 210 | final Issue alarm1 = report.get(0); 211 | assertEquals(fileName, Severity.WARNING_HIGH, alarm1.getSeverity()); 212 | assertEquals(fileName, "Incompatible object pointer conversion [Failed coding rule checks]", alarm1.getCategory()); 213 | assertEquals(fileName, "preprocessed/src/dhry/Proc1.c", alarm1.getFileName()); 214 | assertEquals(fileName, 77, alarm1.getLineStart()); 215 | assertEquals(fileName, 44, alarm1.getColumnStart()); 216 | assertEquals(fileName, 77, alarm1.getLineEnd()); 217 | assertEquals(fileName, 54, alarm1.getColumnEnd()); 218 | if (fileName.compareTo("report-19.10.xml") <= 0) { 219 | assertEquals(fileName, "[ conversion between two incompatible pointer types: from <RecordType*> (aka <struct Record*>) to <char*>
Code:
" + 230 | "memcpy_x(&((*(PtrParIn->PtrComp))), &(*PtrGlb), sizeof((*(PtrParIn->PtrComp))));\n" + 231 | " ~~~~~~~~~~", 232 | alarm1.getDescription()); 233 | 234 | final Issue alarm2 = report.get(119); 235 | assertEquals(fileName, Severity.WARNING_HIGH, alarm2.getSeverity()); 236 | assertEquals(fileName, "Integer division by zero [Division or modulo by zero]", alarm2.getCategory()); 237 | assertEquals(fileName, "preprocessed/src/dhry/Proc7.c", alarm2.getFileName()); 238 | assertEquals(fileName, 78, alarm2.getLineStart()); 239 | assertEquals(fileName, 12, alarm2.getColumnStart()); 240 | assertEquals(fileName, 78, alarm2.getLineEnd()); 241 | assertEquals(fileName, 22, alarm2.getColumnEnd()); 242 | if (fileName.compareTo("report-20.04.xml") <= 0) { 243 | assertEquals(fileName, "ALARM (A): integer division by zero {0}", alarm2.getMessage()); 244 | } else { 245 | assertEquals(fileName, "ALARM (A) int_division_by_zero: divisor in {0}", alarm2.getMessage()); 246 | } 247 | assertEquals(fileName, "n/A", alarm2.getReference()); 248 | assertTrue(fileName, alarm2.getDescription().startsWith("
Code:
IntLoc = IntParI1/0;\n ~~~~~~~~~~
Context:
l"));
249 | assertTrue(fileName, alarm2.getDescription().endsWith("#call#Proc7"));
250 |
251 | final Issue error1 = report.get(8);
252 | assertEquals(fileName, Severity.ERROR, error1.getSeverity());
253 | if (fileName.compareTo("report-20.10.xml") <= 0) {
254 | assertEquals(fileName, "Definite runtime error [Errors]", error1.getCategory());
255 | if (fileName.compareTo("report-20.04.xml") <= 0) {
256 | assertEquals(fileName, "ERROR: Definite runtime error during assignment in this context. Analysis stopped for this context.", error1.getMessage());
257 | } else {
258 | assertEquals(fileName, "ERROR: Definite runtime error during assignment in this context. Analysis stopped for this context", error1.getMessage());
259 | }
260 | } else {
261 | assertEquals(fileName, "Analysis stopped [Errors]", error1.getCategory());
262 | assertEquals(fileName, "ERROR analysis_stopped: Definite runtime error during assignment in this context. Analysis stopped for this context", error1.getMessage());
263 | }
264 | assertEquals(fileName, "preprocessed/src/scenarios.c", error1.getFileName());
265 | assertEquals(fileName, 73, error1.getLineStart());
266 | assertEquals(fileName, 8, error1.getColumnStart());
267 | assertEquals(fileName, 73, error1.getLineEnd());
268 | assertEquals(fileName, 25, error1.getColumnEnd());
269 | assertEquals(fileName, "n/A", error1.getReference());
270 | if (fileName.compareTo("report-20.04.xml") <= 0) {
271 | assertTrue(fileName, error1.getDescription().startsWith("Context:
l"));
272 | } else {
273 | assertTrue(fileName, error1.getDescription().startsWith("Code:
ArrayBlock[i] = i;\n~~~~~~~~~~~~~~~~~
Context:
l"));
274 | }
275 | assertTrue(fileName, error1.getDescription().endsWith(""));
276 |
277 | final Issue error2 = report.get(124);
278 | assertEquals(fileName, Severity.ERROR, error2.getSeverity());
279 | if (fileName.compareTo("report-20.10.xml") <= 0) {
280 | assertEquals(fileName, "Definite runtime error [Errors]", error2.getCategory());
281 | } else {
282 | assertEquals(fileName, "Analysis stopped [Errors]", error2.getCategory());
283 | }
284 | assertEquals(fileName, "preprocessed/src/dhry/Proc7.c", error2.getFileName());
285 | assertEquals(fileName, 78, error2.getLineStart());
286 | assertEquals(fileName, 3, error2.getColumnStart());
287 | assertEquals(fileName, 78, error2.getLineEnd());
288 | assertEquals(fileName, 22, error2.getColumnEnd());
289 | if (fileName.compareTo("report-20.04.xml") <= 0) {
290 | assertEquals(fileName, "ERROR: Definite runtime error during assignment in this context. Analysis stopped for this context.", error2.getMessage());
291 | } else if (fileName.compareTo("report-20.10.xml") <= 0) {
292 | assertEquals(fileName, "ERROR: Definite runtime error during assignment in this context. Analysis stopped for this context", error2.getMessage());
293 | } else {
294 | assertEquals(fileName, "ERROR analysis_stopped: Definite runtime error during assignment in this context. Analysis stopped for this context", error2.getMessage());
295 | }
296 | assertEquals(fileName, "n/A", error2.getReference());
297 | if (fileName.compareTo("report-20.04.xml") <= 0) {
298 | assertTrue(fileName, error2.getDescription().startsWith("Context:
l"));
299 | } else {
300 | assertTrue(fileName, error2.getDescription().startsWith("Code:
IntLoc = IntParI1/0;\n~~~~~~~~~~~~~~~~~~~
Context:
l"));
301 | }
302 | assertTrue(fileName, error2.getDescription().endsWith(""));
303 | } else {
304 | assertEquals(fileName, 127, report.size());
305 |
306 | final Issue alarm1 = report.get(0);
307 | assertEquals(fileName, Severity.WARNING_HIGH, alarm1.getSeverity());
308 | assertEquals(fileName, "Incompatible object pointer conversion [Failed coding rule checks]", alarm1.getCategory());
309 | assertEquals(fileName, "preprocessed/src/dhry/Proc1.c", alarm1.getFileName());
310 | assertEquals(fileName, 77, alarm1.getLineStart());
311 | assertEquals(fileName, 44, alarm1.getColumnStart());
312 | assertEquals(fileName, 77, alarm1.getLineEnd());
313 | assertEquals(fileName, 54, alarm1.getColumnEnd());
314 | assertEquals(fileName, "conversion between two incompatible pointer types: from <RecordType *> (aka <struct Record *>) to <char *>
ALARM (R) check_incompatible_object_pointer_conversion: check failed (violates A.1.11)", alarm1.getMessage());
315 | assertEquals(fileName, "n/A", alarm1.getReference());
316 | assertEquals(fileName,
317 | "Code:
" +
318 | "memcpy_x(&((*(PtrParIn->PtrComp))), &(*PtrGlb), sizeof((*(PtrParIn->PtrComp))));\n" +
319 | " ~~~~~~~~~~
",
320 | alarm1.getDescription());
321 |
322 | final Issue error1 = report.get(120);
323 | assertEquals(fileName, Severity.ERROR, error1.getSeverity());
324 | assertEquals(fileName, "Analysis stopped in critical context [Errors]", error1.getCategory());
325 | assertEquals(fileName, "preprocessed/src/dhry/Proc7.c", error1.getFileName());
326 | assertEquals(fileName, 78, error1.getLineStart());
327 | assertEquals(fileName, 3, error1.getColumnStart());
328 | assertEquals(fileName, 78, error1.getLineEnd());
329 | assertEquals(fileName, 22, error1.getColumnEnd());
330 | assertEquals(fileName, "ERROR analysis_stopped: Definite runtime error during assignment in this context. Analysis stopped for this context", error1.getMessage());
331 | assertEquals(fileName, "n/A", error1.getReference());
332 | assertTrue(fileName, error1.getDescription().startsWith("Code:
IntLoc = IntParI1/0;\n~~~~~~~~~~~~~~~~~~~
Context:
l"));
333 | assertTrue(fileName, error1.getDescription().endsWith("#call#Proc7"));
334 |
335 | final Issue error2 = report.get(124);
336 | assertEquals(fileName, Severity.ERROR, error2.getSeverity());
337 | assertEquals(fileName, "Analysis stopped in critical context [Errors]", error2.getCategory());
338 | assertEquals(fileName, "preprocessed/src/dhry/Proc7.c", error2.getFileName());
339 | assertEquals(fileName, 78, error2.getLineStart());
340 | assertEquals(fileName, 3, error2.getColumnStart());
341 | assertEquals(fileName, 78, error2.getLineEnd());
342 | assertEquals(fileName, 22, error2.getColumnEnd());
343 | assertEquals(fileName, "ERROR analysis_stopped: Definite runtime error during assignment in this context. Analysis stopped for this context", error2.getMessage());
344 | assertEquals(fileName, "n/A", error2.getReference());
345 | assertTrue(fileName, error2.getDescription().startsWith("Code:
IntLoc = IntParI1/0;\n~~~~~~~~~~~~~~~~~~~
Context:
l"));
346 | assertTrue(fileName, error2.getDescription().endsWith("#call#Proc7"));
347 | }
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/src/main/java/com/absint/astree/AstreeBuilder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2016, AbsInt Angewandte Informatik GmbH
5 | * Author: Dr.-Ing. Joerg Herter
6 | * Email: herter@absint.com
7 | *
8 | * Permission is hereby granted, free of charge, to any person obtaining a copy
9 | * of this software and associated documentation files (the "Software"), to deal
10 | * in the Software without restriction, including without limitation the rights
11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | * copies of the Software, and to permit persons to whom the Software is
13 | * furnished to do so, subject to the following conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be included in
16 | * all copies or substantial portions of the Software.
17 | *
18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | * THE SOFTWARE.
25 | */
26 |
27 | package com.absint.astree;
28 | import hudson.Proc;
29 | import hudson.Launcher;
30 | import hudson.EnvVars;
31 | import hudson.Extension;
32 | import hudson.FilePath;
33 | import hudson.util.ArgumentListBuilder;
34 | import hudson.util.FormValidation;
35 | import hudson.model.AbstractProject;
36 | import hudson.model.Run;
37 | import hudson.model.TaskListener;
38 | import hudson.tasks.Builder;
39 | import hudson.tasks.BuildStepDescriptor;
40 | import jenkins.tasks.SimpleBuildStep;
41 | import net.sf.json.JSONObject;
42 | import org.kohsuke.stapler.DataBoundConstructor;
43 | import org.kohsuke.stapler.StaplerRequest;
44 | import org.kohsuke.stapler.QueryParameter;
45 |
46 | import javax.servlet.ServletException;
47 | import java.io.*;
48 | import java.util.*;
49 | import java.util.regex.Pattern;
50 | import java.util.regex.Matcher;
51 |
52 |
53 | /**
54 | *
55 | * When the user configures the project and enables this builder,
56 | * {@link DescriptorImpl#newInstance(StaplerRequest)} is invoked
57 | * and a new {@link AstreeBuilder} is created. The created
58 | * instance is persisted to the project configuration XML by using
59 | * XStream, so this allows you to use instance fields
60 | * to remember the configuration.
61 | *
62 | * When a build is performed, the {@link #perform} method will be invoked.
63 | *
64 | * @author AbsInt Angewandte Informatik GmbH
65 | */
66 | public class AstreeBuilder extends Builder implements SimpleBuildStep {
67 | private static final String PLUGIN_NAME = "Astrée for C Jenkins PlugIn";
68 | private static final String BUILD_NR = "24.10";
69 |
70 | private static final String TMP_REPORT_FILE = "absint_astree_analysis_report";
71 | private static final String TMP_PREPROCESS_OUTPUT = "absint_astree_preprocess_output.txt";
72 |
73 | private String dax_file, output_dir, analysis_id;
74 | private FailonSwitch failonswitch;
75 | private boolean genXMLOverview, genXMLCoverage, genXMLAlarmsByOccurence,
76 | genXMLAlarmsByCategory, genXMLAlarmsByFile, genXMLRulechecks,
77 | genPreprocessOutput, dropAnalysis;
78 | private boolean skip_analysis;
79 |
80 | private Proc proc; // reference to an associated a3c client process
81 |
82 | // Fields in config.jelly must match the parameter names in the "DataBoundConstructor"
83 | @DataBoundConstructor
84 | public AstreeBuilder( String dax_file, String analysis_id, String output_dir, boolean skip_analysis,
85 | boolean genXMLOverview, boolean genXMLCoverage, boolean genXMLAlarmsByOccurence,
86 | boolean genXMLAlarmsByCategory, boolean genXMLAlarmsByFile, boolean genXMLRulechecks,
87 | boolean dropAnalysis, boolean genPreprocessOutput, FailonSwitch failonswitch,
88 | AnalysisServerConfiguration analysisSrv
89 | )
90 | {
91 | this.dax_file = dax_file;
92 | this.analysis_id = analysis_id;
93 | this.output_dir = output_dir;
94 | this.skip_analysis = skip_analysis;
95 | this.failonswitch = failonswitch;
96 |
97 | this.genXMLOverview = genXMLOverview;
98 | this.genXMLCoverage = genXMLCoverage;
99 | this.genXMLAlarmsByOccurence = genXMLAlarmsByOccurence;
100 | this.genXMLAlarmsByCategory = genXMLAlarmsByCategory;
101 | this.genXMLAlarmsByFile = genXMLAlarmsByFile;
102 | this.genXMLRulechecks = genXMLRulechecks;
103 |
104 | this.dropAnalysis = dropAnalysis;
105 | this.genPreprocessOutput = genPreprocessOutput;
106 | }
107 |
108 | /*
109 | * Interface to config.jelly.
110 | */
111 |
112 | /**
113 | * Returns the currently set path to the DAX file used for the analysis run.
114 | *
115 | * @return java.lang.String
116 | */
117 | public String getDax_file() {
118 | return dax_file;
119 | }
120 |
121 | /**
122 | * Returns the currently set analysis ID used for the analysis run.
123 | *
124 | * @return java.lang.String
125 | */
126 | public String getAnalysis_id() {
127 | return analysis_id;
128 | }
129 |
130 | /**
131 | * Returns the currently set path used as output directory for the analyses.
132 | *
133 | * @return java.lang.String
134 | */
135 | public String getOutput_dir() {
136 | return output_dir;
137 | }
138 |
139 | /**
140 | * Indicates whether the analysis run is configured to potentially fail a build.
141 | *
142 | * @return boolean
143 | */
144 | public boolean isFailonswitch() {
145 | return (this.failonswitch != null);
146 | }
147 |
148 | /**
149 | * @return java.lang.String
150 | */
151 | public String getFailon() {
152 | if(this.failonswitch == null) return "";
153 | return this.failonswitch.getFailon();
154 | }
155 |
156 | /**
157 | * Indicates whether the analysis run is configured to
158 | * be temporarily skipped (i.e., no analysis is to be done).
159 | *
160 | * @return boolean
161 | */
162 | public boolean isSkip_analysis() {
163 | return this.skip_analysis;
164 | }
165 |
166 | /**
167 | * Indicates whether the analysis run is configured to produce the
168 | * XML overview summary.
169 | *
170 | * @return boolean
171 | */
172 | public boolean isGenXMLOverview() {
173 | return this.genXMLOverview;
174 | }
175 |
176 | /**
177 | * Indicates whether the analysis run is configured to produce the
178 | * XML coverage summary.
179 | *
180 | * @return boolean
181 | */
182 | public boolean isGenXMLCoverage() {
183 | return this.genXMLCoverage;
184 | }
185 |
186 | /**
187 | * Indicates whether the analysis run is configured to produce the
188 | * XML alarms-by-occurence summary.
189 | *
190 | * @return boolean
191 | */
192 | public boolean isGenXMLAlarmsByOccurence() {
193 | return this.genXMLAlarmsByOccurence;
194 | }
195 |
196 | /**
197 | * Indicates whether the analysis run is configured to produce the
198 | * XML alarms-by-category summary.
199 | *
200 | * @return boolean
201 | */
202 | public boolean isGenXMLAlarmsByCategory() {
203 | return this.genXMLAlarmsByCategory;
204 | }
205 |
206 | /**
207 | * Indicates whether the analysis run is configured to produce the
208 | * XML alarms-by-file summary.
209 | *
210 | * @return boolean
211 | */
212 | public boolean isGenXMLAlarmsByFile() {
213 | return this.genXMLAlarmsByFile;
214 | }
215 |
216 | /**
217 | * Indicates whether the analysis run is configured to produce the
218 | * XML rule checks summary.
219 | *
220 | * @return boolean
221 | */
222 | public boolean isGenXMLRulechecks() {
223 | return this.genXMLRulechecks;
224 | }
225 |
226 | /**
227 | * Indicates whether the analysis run is configured to produce the
228 | * (text) preprocess output report.
229 | *
230 | * @return boolean
231 | */
232 | public boolean isGenPreprocessOutput() {
233 | return this.genPreprocessOutput;
234 | }
235 |
236 | /**
237 | * Indicates whether the project is to be deleted on the server after
238 | * the analysis run.
239 | *
240 | * @return boolean
241 | */
242 | public boolean isDropAnalysis() {
243 | return this.dropAnalysis;
244 | }
245 |
246 |
247 | /*
248 | * end interface to config.jelly.
249 | */
250 |
251 |
252 | /**
253 | * Expands environment variables of the form
254 | * ${VAR_NAME}
255 | * by their current value.
256 | *
257 | * @param cmdln the java.lang.String, usually a command line,
258 | * in which to expand variables
259 | * @param envMap a java.util.Map containing the environment variables
260 | * and their current values
261 | * @param isUnix boolean
262 | * @return the input String with environment variables expanded to their current value
263 | */
264 | private static final String expandEnvironmentVarsHelper(
265 | String cmdln, Map envMap, boolean isUnix ) {
266 | final String pattern = "\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}";
267 | final Pattern expr = Pattern.compile(pattern);
268 | Matcher matcher = expr.matcher(cmdln);
269 | String envValue;
270 | Pattern subexpr;
271 | while (matcher.find()) {
272 | envValue = envMap.get(matcher.group(1).toUpperCase());
273 | if (envValue == null) {
274 | envValue = "";
275 | } else {
276 | envValue = envValue.replace("\\", "\\\\");
277 | }
278 | subexpr = Pattern.compile(Pattern.quote(matcher.group(0)));
279 | cmdln = subexpr.matcher(cmdln).replaceAll(envValue);
280 | }
281 |
282 | if(isUnix) {
283 | return cmdln.replace('\\','/');
284 | } else {
285 | return cmdln.replace('/','\\');
286 | }
287 | }
288 |
289 |
290 | /**
291 | */
292 | private ArgumentListBuilder constructCommandLineCall(String reportfile, EnvVars envVars, boolean isUnix, FilePath preprocessOutput) {
293 | final ArgumentListBuilder builder = new ArgumentListBuilder();
294 |
295 | builder.add(getDescriptor().getAlauncher());
296 | builder.add("-b");
297 | builder.add("-s", expandEnvironmentVarsHelper(getDescriptor().getAstree_server(), envVars, isUnix));
298 | if (analysis_id != null && !analysis_id.trim().isEmpty()) {
299 | builder.add("--id", expandEnvironmentVarsHelper(analysis_id, envVars, isUnix));
300 | }
301 | if (dax_file != null && !dax_file.trim().isEmpty()) {
302 | builder.add("--import", expandEnvironmentVarsHelper(dax_file, envVars, isUnix));
303 | }
304 | builder.add("--report-file", reportfile + ".txt");
305 | builder.add("--xml-report-file", reportfile + ".xml");
306 | if (genPreprocessOutput) {
307 | builder.add("--preprocess-report-file", preprocessOutput.getRemote());
308 | }
309 | if (dropAnalysis) {
310 | builder.add("--drop");
311 | }
312 |
313 | return builder;
314 | }
315 |
316 |
317 | @Override
318 | public void perform(Run,?> build, FilePath workspace, Launcher launcher, TaskListener listener) {
319 | int exitCode = -1;
320 | // Set some defaults and parameters.
321 | if(output_dir == null || output_dir.equals(""))
322 | output_dir = workspace.toString();
323 | String reportfile = workspace.toString() + (launcher.isUnix() ? "/" : "\\") + TMP_REPORT_FILE;
324 |
325 | FilePath rfile;
326 | try {
327 | // Analysis run started. ID plugin in Jenkins output.
328 | listener.getLogger().println("This is " + PLUGIN_NAME + " in version " + BUILD_NR);
329 | // Clear log file
330 | rfile = new FilePath(workspace, TMP_REPORT_FILE + ".txt");
331 | if( rfile.delete() )
332 | listener.getLogger().println("Old log file erased.");
333 | rfile.touch(System.currentTimeMillis());
334 | listener.getLogger().println("New log file created.");
335 | // Create log file reader thread
336 | StatusPoller sp = new StatusPoller(1000, listener, rfile);
337 |
338 | if(this.skip_analysis) {
339 | listener.getLogger().println("Analysis run has been (temporarily) deactivated. Skipping analysis run.");
340 | return; // nothing to do, exit method.
341 | }
342 |
343 | // Print some configuration info.
344 | if(failonswitch != null)
345 | listener.getLogger().println( "Astrée fails build on " + failonswitch.getFailon() );
346 |
347 | String infoStringSummaryDest = "Summary reports will be generated in " + output_dir;
348 | infoStringSummaryDest = expandEnvironmentVarsHelper(
349 | "Summary reports will be generated in " + output_dir,
350 | build.getEnvironment(listener),
351 | launcher.isUnix());
352 | listener.getLogger().println(infoStringSummaryDest);
353 |
354 | final EnvVars envVars = build.getEnvironment(listener);
355 | if (!getDescriptor().getUser().trim().isEmpty() && !getDescriptor().getPassword().trim().isEmpty()) {
356 | envVars.put("A3_SERVER_USER", getDescriptor().getUser());
357 | envVars.put("A3_SERVER_PASSWORD", getDescriptor().getPassword());
358 | }
359 |
360 | sp.start(); // Start log file reader
361 | proc = launcher.launch()
362 | .cmds(constructCommandLineCall(reportfile, build.getEnvironment(listener), launcher.isUnix(), workspace.child(TMP_PREPROCESS_OUTPUT)))
363 | .envs(envVars)
364 | .stdout(listener.getLogger())
365 | .pwd(workspace)
366 | .start();
367 | exitCode = proc.join(); // Wait for Astree to finish
368 | sp.kill(); // Stop log file reader
369 | sp.join(); // Wait for log file reader to finish
370 | if(exitCode == 0)
371 | listener.getLogger().println("Analysis run succeeded.");
372 | else
373 | listener.getLogger().println("Analysis run failed.");
374 | } catch (IOException e) {
375 | e.printStackTrace();
376 | listener.getLogger().println("IOException caught during analysis run.");
377 | } catch (InterruptedException e) {
378 | e.printStackTrace();
379 | listener.getLogger().println("InterruptedException caught during analysis run.");
380 | }
381 | if(exitCode == 0) { // activities after successful analysis run
382 | /* Read analysis summary and
383 | check whether Astrée shall fail the build due to reported errors etc */
384 | AnalysisSummary summary = AnalysisSummary.readFromReportFile(reportfile + ".txt");
385 | if( failonswitch != null && failonswitch.failOnErrors()
386 | && summary.getNumberOfErrors() > 0) {
387 | listener.getLogger().println( "Errors reported! Number of errors: " +
388 | summary.getNumberOfErrors());
389 | build.setResult(hudson.model.Result.FAILURE);
390 | }
391 | else if( failonswitch != null && failonswitch.failOnAlarms()
392 | && summary.getNumberOfAlarms() > 0) {
393 | listener.getLogger().println( "Alarms reported! Number of alarms: " +
394 | summary.getNumberOfAlarms());
395 |
396 | build.setResult(hudson.model.Result.FAILURE);
397 | }
398 | else if( failonswitch != null && failonswitch.failOnFlowAnomalies()
399 | && ( summary.getNumberOfFlowAnomalies()
400 | + summary.getNumberOfAlarms() > 0) ) {
401 | build.setResult(hudson.model.Result.FAILURE);
402 | }
403 | } else { // activities after unsuccessful analysis run
404 | // If Astrée cannot be invoked, conservatively fail the build...
405 | build.setResult(hudson.model.Result.FAILURE);
406 | }
407 | }
408 |
409 | /**
410 | * Override finalize method to ensure existing a3c client processes are killed upon destruction
411 | * of AstreeBuilder objects.
412 | */
413 | protected void finalize() {
414 | try {
415 | if(proc != null)
416 | proc.kill();
417 | } catch(Exception e) {
418 | }
419 | }
420 |
421 | // Overridden for better type safety.
422 | // If your plugin doesn't really define any property on Descriptor,
423 | // you don't have to do this.
424 | @Override
425 | public DescriptorImpl getDescriptor() {
426 | return (DescriptorImpl)super.getDescriptor();
427 | }
428 |
429 |
430 |
431 | private void copyText2PrintStream( PrintStream dest, String srcPath ) {
432 | dest.println("Appending analysis report.");
433 | try{
434 | BufferedReader br = new BufferedReader(
435 | new InputStreamReader(
436 | new FileInputStream(srcPath), "UTF-8" ));
437 | String line = br.readLine() ;
438 | while(line != null) {
439 | dest.println(line);
440 | line = br.readLine();
441 | }
442 | br.close();
443 | } catch(IOException e) {
444 | }
445 | }
446 |
447 |
448 | /**
449 | * Descriptor for {@link AstreeBuilder}. Used as a singleton.
450 | * The class is marked as public so that it can be accessed from views.
451 | *
452 | *
453 | */
454 | @Extension // This indicates to Jenkins that this is an implementation of an extension point.
455 | public static final class DescriptorImpl extends BuildStepDescriptor {
456 | /*
457 | * To persist global configuration information,
458 | * simply store it in a field and call save().
459 | *
460 | *
461 | * If you don't want fields to be persisted, use "transient".
462 | */
463 |
464 | /*
465 | * Properties set by the Astree configuration mask:
466 | * Jenkins~~~Manage Jenkins~~~Configure System
467 | */
468 | private String alauncher;
469 | private String astree_server;
470 | private String user, password;
471 |
472 | /**
473 | * Constructor.
474 | *
475 | * Constructs a new object of this class and
476 | * loads the persisted global configuration.
477 | */
478 | public DescriptorImpl() {
479 | load();
480 | }
481 |
482 | /**
483 | * Return the human readable name used in the configuration screen.
484 | *
485 | * @return java.lang.String
486 | */
487 | public String getDisplayName() {
488 | return "Astrée Analysis Run";
489 | }
490 |
491 |
492 | /**
493 | * Performs on-the-fly validation of the form field 'astree_server'.
494 | *
495 | * @param value The value that the user has typed.
496 | * @return
497 | * Indicates the outcome of the validation. This is sent to the browser.
498 | *
499 | * Note that returning {@link FormValidation#error(String)} does not
500 | * prevent the form from being saved. It just means that a message
501 | * will be displayed to the user.
502 | * @throws IOException as super class
503 | * @throws ServletException as super class
504 | **/
505 | public FormValidation doCheckAstree_server(@QueryParameter String value)
506 | throws IOException, ServletException {
507 | if(value == null || value.trim().equals("") )
508 | return FormValidation.error("Please set a valid server of form :");
509 | if ( !( value.matches("[a-zA-Z][a-zA-Z0-9\\.\\-]{0,22}[a-zA-Z0-9]:\\d{1,5}") /* hostname */
510 | || value.matches("(\\d{1,3}\\.){3,3}\\d{1,3}:\\d{1,5}" /* ip address */) ) )
511 | return FormValidation.warning("The Astrée Server needs to be specified as a hostname followed by a colon followed by a port number.");
512 | return FormValidation.ok();
513 | }
514 |
515 | /**
516 | * Performs on-the-fly validation of the form field 'alauncher'.
517 | *
518 | * @param value The value that the user has typed.
519 | * @param project unused
520 | * @return
521 | * Indicates the outcome of the validation. This is sent to the browser.
522 | *
523 | * Note that returning {@link FormValidation#error(String)} does not
524 | * prevent the form from being saved. It just means that a message
525 | * will be displayed to the user.
526 | * @throws IOException as super class
527 | * @throws ServletException as super class
528 | **/
529 | public FormValidation doCheckAlauncher(@QueryParameter String value, AbstractProject project)
530 | throws IOException, ServletException {
531 | if(value == null || value.trim().equals("") )
532 | return FormValidation.error("No file specified.");
533 |
534 | File ftmp = new File(value);
535 | if (!ftmp.exists())
536 | return FormValidation.error("Specified file not found.");
537 | if(!ftmp.isFile())
538 | return FormValidation.error("Specified file is not a normal file.");
539 | if (!ftmp.canExecute())
540 | return FormValidation.error("Specified file cannot be executed.");
541 | return FormValidation.ok();
542 | }
543 |
544 |
545 |
546 | /**
547 | * Helper method to check whether a string contains an environment variable of form
548 | *
${IDENTIFIER}
549 | *
550 | * @param s String to scan for environment variable expressions
551 | * @return Outcome of the check as a boolean (true if such an expression
552 | * was found, otherwise false).
553 | */
554 | public static final boolean containsEnvVars(String s)
555 | {
556 | final String pattern = "\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}";
557 | final Pattern expr = Pattern.compile(pattern);
558 | Matcher matcher = expr.matcher(s);
559 | return matcher.find();
560 | }
561 |
562 |
563 | /**
564 | * Performs on-the-fly validation of the form field 'dax_file'.
565 | *
566 | * @param value
567 | * This parameter receives the value that the user has typed.
568 | * @return
569 | * Indicates the outcome of the validation. This is sent to the browser.
570 | *
571 | * Note that returning {@link FormValidation#error(String)} does not
572 | * prevent the form from being saved. It just means that a message
573 | * will be displayed to the user.
574 | * @throws IOException as super class
575 | * @throws ServletException as super class
576 | **/
577 | public FormValidation doCheckDax_file(@QueryParameter String value)
578 | throws IOException, ServletException {
579 | if (
580 | (value == null || value.trim().equals(""))
581 | )
582 | return FormValidation.warning("No DAX file specified. Only ID will be used.");
583 |
584 | if(containsEnvVars(value)) {
585 | return FormValidation.warning("The specified path contains an environment variable, please make sure the constructed paths are correct.");
586 | }
587 |
588 | File ftmp = new File(value);
589 | if (!ftmp.exists())
590 | return FormValidation.error("Specified file not found.");
591 | if (!ftmp.canRead())
592 | return FormValidation.error("Specified file cannot be read.");
593 | if (!value.endsWith(".dax"))
594 | return FormValidation.warning("The specified file exists, but does not have the expected suffix (.dax).");
595 |
596 | return FormValidation.ok();
597 | }
598 |
599 | /**
600 | * Performs on-the-fly validation of the form field 'analysis_id'.
601 | *
602 | * @param value
603 | * This parameter receives the value that the user has typed.
604 | * @return
605 | * Indicates the outcome of the validation. This is sent to the browser.
606 | *
607 | * Note that returning {@link FormValidation#error(String)} does not
608 | * prevent the form from being saved. It just means that a message
609 | * will be displayed to the user.
610 | * @throws IOException as super class
611 | * @throws ServletException as super class
612 | **/
613 | public FormValidation doCheckAnalysis_id(@QueryParameter String value)
614 | throws IOException, ServletException {
615 | if (
616 | (value == null || value.trim().equals(""))
617 | )
618 | return FormValidation.warning("No ID specified. Only DAX file will be used.");
619 |
620 | if(containsEnvVars(value)) {
621 | return FormValidation.warning("The ID contains an environment variable, please make sure that the constructed IDs are valid.");
622 | }
623 |
624 |
625 | if(!value.matches("\\d*"))
626 | return FormValidation.error("ID is not valid.");
627 |
628 | return FormValidation.ok();
629 | }
630 |
631 | /**
632 | * Performs on-the-fly validation of the form field 'output_dir'.
633 | *
634 | * @param value
635 | * This parameter receives the value that the user has typed.
636 | * @return
637 | * Indicates the outcome of the validation. This is sent to the browser.
638 | *
639 | * Note that returning {@link FormValidation#error(String)} does not
640 | * prevent the form from being saved. It just means that a message
641 | * will be displayed to the user.
642 | * @throws IOException as super class
643 | * @throws ServletException as super class
644 | **/
645 | public FormValidation doCheckOutput_dir(@QueryParameter String value)
646 | throws IOException, ServletException {
647 | if(value == null || value.trim().equals("") )
648 | return FormValidation.warning("No directory specified.");
649 |
650 | if(containsEnvVars(value)) {
651 | return FormValidation.warning("The specified path contains an environment variable, please make sure that the constructed paths are correct.");
652 | }
653 |
654 | File ftmp = new File(value);
655 | if (!ftmp.exists())
656 | return FormValidation.error("Specified directory not found.");
657 | if (!ftmp.isDirectory())
658 | return FormValidation.error("Specified path is no directory.");
659 | if (!ftmp.canRead() || !ftmp.canWrite())
660 | return FormValidation.warning("No permissions to read/write the specified directory.");
661 |
662 | return FormValidation.ok();
663 | }
664 |
665 |
666 |
667 | /**
668 | * Indicates that this builder can be used with all kinds of project types.
669 | *
670 | * @return boolean
671 | */
672 | public boolean isApplicable(Class extends AbstractProject> aClass) {
673 | return true;
674 | }
675 |
676 | /**
677 | * Sets a new configuration.
678 | *
679 | * @throws FormException as super class
680 | */
681 | @Override
682 | public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
683 | // To persist global configuration information,
684 | // set that to properties and call save().
685 | this.alauncher = formData.getString("alauncher");
686 | this.astree_server = formData.getString("astree_server");
687 | this.user = formData.getString("user");
688 | this.password = formData.getString("password");
689 | // ... data set, so call save():
690 | save();
691 | return super.configure(req,formData);
692 | }
693 |
694 | /**
695 | * Returns the currently configured Astrée server
696 | * (as host:port).
697 | *
698 | * @return java.lang.String
699 | */
700 | public String getAstree_server() {
701 | return this.astree_server;
702 | }
703 |
704 | /**
705 | * Returns the currently configured alauncher.
706 | *
707 | * @return java.lang.String
708 | */
709 | public String getAlauncher() {
710 | return this.alauncher;
711 | }
712 |
713 |
714 | /**
715 | * Returns the currently configured Astrée user.
716 | *
717 | * @return java.lang.String
718 | */
719 | public String getUser() {
720 | return this.user;
721 | }
722 |
723 | /**
724 | * Returns the currently configured Astrée (user) password.
725 | *
726 | * @return java.lang.String
727 | */
728 | public String getPassword() {
729 | return this.password;
730 | }
731 | }
732 | }
733 |
--------------------------------------------------------------------------------