Skip to content

Commit fed4bad

Browse files
authored
feat: acceptance tests (#848)
1 parent 2479fb4 commit fed4bad

File tree

25 files changed

+2458
-49
lines changed

25 files changed

+2458
-49
lines changed

.github/workflows/acceptance_test.yml

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ jobs:
5454
- name: Prepare version name
5555
id: prep
5656
run: |
57-
echo ${{ github.event.head_commit.message }}
5857
VERSION_TAG=edge
5958
if [[ $GITHUB_REF == refs/tags/* ]]; then
6059
VERSION_TAG=${GITHUB_REF#refs/tags/}
@@ -88,6 +87,11 @@ jobs:
8887
with:
8988
name: gtfs-validator-snapshot
9089
path: main/build/libs/gtfs-validator-${{ steps.prep.outputs.versionTag }}_cli.jar
90+
- name: Persist comparator snapshot jar
91+
uses: actions/upload-artifact@v2
92+
with:
93+
name: comparator-snapshot
94+
path: output-comparator/build/libs/output-comparator-${{ steps.prep.outputs.versionTag }}_cli.jar
9195
pack-master:
9296
needs: [ validate-gradle-wrapper ]
9397
runs-on: ubuntu-latest
@@ -178,21 +182,58 @@ jobs:
178182
env:
179183
OUTPUT_BASE: ${{ github.sha }}
180184
- name: Persist reports
181-
if: always()
182185
uses: actions/upload-artifact@v2
183186
with:
184187
name: reports_all
185188
path: ${{ github.sha }}/output
186-
- name: Set up Cloud SDK
187-
uses: google-github-actions/setup-gcloud@master
189+
- name: Set up and authorize Cloud
190+
uses: google-github-actions/auth@v0
188191
with:
189-
project_id: ${{ secrets.VALIDATOR_PROJECT_ID }}
190-
service_account_key: ${{ secrets.VALIDATOR_SA_KEY }}
191-
export_default_credentials: true
192+
credentials_json: ${{ secrets.VALIDATOR_SA_KEY }}
192193
- name: Upload reports to Google Cloud Storage
193194
id: upload-files
194-
if: always()
195195
uses: google-github-actions/upload-cloud-storage@main
196196
with:
197197
path: ${{ github.sha }}/output
198198
destination: gtfs-validator-reports
199+
compare-outputs:
200+
needs: [ get-reports ]
201+
runs-on: ubuntu-latest
202+
steps:
203+
- uses: actions/checkout@v1
204+
- name: Download comparator .jar file from previous job
205+
uses: actions/download-artifact@v2
206+
with:
207+
name: comparator-snapshot
208+
- name: Retrieve reports from previous job
209+
uses: actions/download-artifact@v2
210+
with:
211+
name: reports_all
212+
- name: Retrieve gtfs latest versions from previous job
213+
uses: actions/download-artifact@v2
214+
with:
215+
name: datasets_metadata
216+
- name: Generate acceptance report test
217+
run: |
218+
java -jar output-comparator*.jar --report_directory . --new_error_threshold 1 --reference_report_name reference.json --latest_report_name latest.json --percent_invalid_datasets_threshold 1 --output_base acceptance-test-output --source_urls gtfs_latest_versions.json --percent_corrupted_sources 2
219+
- name: Persist acceptance test reports
220+
if: always()
221+
uses: actions/upload-artifact@v2
222+
with:
223+
name: acceptance_test_report
224+
path: acceptance-test-output
225+
- name: Generate PR comment
226+
id: generate-comment
227+
if: always()
228+
run: |
229+
python3 scripts/comment_generator.py -u gtfs_latest_versions.json -a acceptance-test-output/acceptance_report.json -c ${{ github.sha }} -r ${{github.run_id}} -x acceptance-test-output/sources_corruption_report.json > pr_comment.txt
230+
PR_COMMENT=$(<pr_comment.txt)
231+
echo "PR_COMMENT<<EOF" >> $GITHUB_ENV
232+
echo "$PR_COMMENT" >> $GITHUB_ENV
233+
echo "EOF" >> $GITHUB_ENV
234+
- name: Comment Pull Request
235+
if: always()
236+
uses: thollander/[email protected]
237+
with:
238+
message: ${{ env.PR_COMMENT }}
239+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ Instructions to build the project from the command-line using [Gradle](https://g
7777
# Architecture
7878
The architecture of the `gtfs-validator` is described on our [Architecture page](/docs/ARCHITECTURE.md).
7979

80+
# Acceptance tests
81+
In order to avoid sudden changes in the validation output that might declare previously valid datasets invalid, all code changes in pull requests are tested against GTFS datasets in the [MobilityDatabase](http://mobilitydatabase.org/wiki/Main_Page). The acceptance test process is described in [ACCEPTANCE_TESTS.md](docs/ACCEPTANCE_TESTS.md).
82+
8083
# License
8184
Code licensed under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0).
8285

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2021 MobilityData IO
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.mobilitydata.gtfsvalidator.io;
18+
19+
import static org.mobilitydata.gtfsvalidator.notice.Notice.GSON;
20+
21+
import com.google.common.reflect.TypeToken;
22+
import com.google.gson.Gson;
23+
import com.google.gson.GsonBuilder;
24+
import com.google.gson.JsonArray;
25+
import com.google.gson.JsonDeserializationContext;
26+
import com.google.gson.JsonDeserializer;
27+
import com.google.gson.JsonElement;
28+
import com.google.gson.JsonObject;
29+
import com.google.gson.internal.LinkedTreeMap;
30+
import java.lang.reflect.Type;
31+
import java.util.ArrayList;
32+
import java.util.Collection;
33+
import java.util.LinkedHashSet;
34+
import java.util.List;
35+
import java.util.Map;
36+
import java.util.Set;
37+
import org.mobilitydata.gtfsvalidator.model.NoticeReport;
38+
import org.mobilitydata.gtfsvalidator.model.ValidationReport;
39+
import org.mobilitydata.gtfsvalidator.notice.Notice;
40+
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
41+
42+
/**
43+
* Used to (de)serialize a JSON validation report. This represents a validation report as a list of
44+
* {@code NoticeReport} which provides information about each notice generated during a GTFS dataset
45+
* validation.
46+
*/
47+
public class ValidationReportDeserializer implements JsonDeserializer<ValidationReport> {
48+
49+
private static final Gson GSON =
50+
new GsonBuilder().serializeNulls().serializeSpecialFloatingPointValues().create();
51+
private static final String NOTICES_MEMBER_NAME = "notices";
52+
53+
@Override
54+
public ValidationReport deserialize(
55+
JsonElement json, Type typoOfT, JsonDeserializationContext context) {
56+
Set<NoticeReport> notices = new LinkedHashSet<>();
57+
JsonObject rootObject = json.getAsJsonObject();
58+
JsonArray noticesArray = rootObject.getAsJsonArray(NOTICES_MEMBER_NAME);
59+
for (JsonElement childObject : noticesArray) {
60+
notices.add(GSON.fromJson(childObject, NoticeReport.class));
61+
}
62+
return new ValidationReport(notices);
63+
}
64+
65+
public static <T extends Notice> JsonObject serialize(
66+
List<T> notices,
67+
int maxExportsPerNoticeTypeAndSeverity,
68+
Map<String, Integer> noticesCountPerTypeAndSeverity) {
69+
Set<NoticeReport> noticeReports = new LinkedHashSet<>();
70+
Gson gson = new Gson();
71+
Type contextType = new TypeToken<Map<String, Object>>() {}.getType();
72+
for (Collection<T> noticesOfType :
73+
NoticeContainer.groupNoticesByTypeAndSeverity(notices).asMap().values()) {
74+
T firstNotice = noticesOfType.iterator().next();
75+
List<LinkedTreeMap<String, Object>> contexts = new ArrayList<>();
76+
int i = 0;
77+
for (T notice : noticesOfType) {
78+
++i;
79+
if (i > maxExportsPerNoticeTypeAndSeverity) {
80+
// Do not export too many notices for this type.
81+
break;
82+
}
83+
contexts.add(gson.fromJson(notice.getContext(), contextType));
84+
}
85+
noticeReports.add(
86+
new NoticeReport(
87+
firstNotice.getCode(),
88+
firstNotice.getSeverityLevel(),
89+
noticesCountPerTypeAndSeverity.get(firstNotice.getMappingKey()),
90+
contexts));
91+
}
92+
return GSON.toJsonTree(new ValidationReport(noticeReports)).getAsJsonObject();
93+
}
94+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2021 MobilityData IO
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.mobilitydata.gtfsvalidator.model;
18+
19+
import com.google.gson.annotations.Expose;
20+
import com.google.gson.internal.LinkedTreeMap;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.Objects;
24+
import org.mobilitydata.gtfsvalidator.notice.SeverityLevel;
25+
26+
/**
27+
* Used to (de)serialize a JSON validation report. It is used to store information about one type of
28+
* notice encountered in a validation report: error code, severity level, the total number of
29+
* notices related to the error code and a list of notice contexts (which provides additional
30+
* information about each notice.
31+
*/
32+
public class NoticeReport {
33+
34+
@Expose() private final String code;
35+
@Expose() private final SeverityLevel severity;
36+
@Expose() private final int totalNotices;
37+
@Expose() private final List<LinkedTreeMap<String, Object>> sampleNotices;
38+
39+
public NoticeReport(
40+
String code,
41+
SeverityLevel severity,
42+
int count,
43+
List<LinkedTreeMap<String, Object>> sampleNotices) {
44+
this.code = code;
45+
this.severity = severity;
46+
this.totalNotices = count;
47+
this.sampleNotices = sampleNotices;
48+
}
49+
50+
public int getTotalNotices() {
51+
return totalNotices;
52+
}
53+
54+
public SeverityLevel getSeverity() {
55+
return severity;
56+
}
57+
58+
public String getCode() {
59+
return code;
60+
}
61+
62+
public List<LinkedTreeMap<String, Object>> getSampleNotices() {
63+
return Collections.unmodifiableList(sampleNotices);
64+
}
65+
66+
public boolean isError() {
67+
return getSeverity().ordinal() >= SeverityLevel.ERROR.ordinal();
68+
}
69+
70+
@Override
71+
public boolean equals(Object other) {
72+
if (this == other) {
73+
return true;
74+
}
75+
if (other instanceof NoticeReport) {
76+
NoticeReport otherNoticeReport = (NoticeReport) other;
77+
return this.getCode().equals(otherNoticeReport.getCode())
78+
&& this.getSeverity().equals(otherNoticeReport.getSeverity())
79+
&& this.getTotalNotices() == (otherNoticeReport.getTotalNotices())
80+
&& getSampleNotices().equals(otherNoticeReport.getSampleNotices());
81+
}
82+
return false;
83+
}
84+
85+
@Override
86+
public int hashCode() {
87+
return Objects.hash(code, severity);
88+
}
89+
}

0 commit comments

Comments
 (0)