• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

creek-service / creek-system-test / #1770

18 Apr 2026 09:29AM UTC coverage: 98.992% (-0.04%) from 99.027%
#1770

Pull #682

github

web-flow
Merge f3ab535df into 24aaaa442
Pull Request #682: Replace deprecated test container mount usage

122 of 123 new or added lines in 12 files covered. (99.19%)

1 existing line in 1 file now uncovered.

1670 of 1687 relevant lines covered (98.99%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.78
/parser/src/main/java/org/creekservice/internal/system/test/parser/YamlTestPackageParser.java
1
/*
2
 * Copyright 2022-2025 Creek Contributors (https://github.com/creek-service)
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.creekservice.internal.system.test.parser;
18

19
import static java.lang.System.lineSeparator;
20
import static java.util.Objects.requireNonNull;
21
import static org.creekservice.api.system.test.model.TestPackage.testPackage;
22

23
import com.fasterxml.jackson.databind.ObjectMapper;
24
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
25
import java.io.IOException;
26
import java.nio.file.FileSystems;
27
import java.nio.file.Files;
28
import java.nio.file.Path;
29
import java.nio.file.PathMatcher;
30
import java.nio.file.Paths;
31
import java.util.Collection;
32
import java.util.List;
33
import java.util.Map;
34
import java.util.Optional;
35
import java.util.function.Function;
36
import java.util.function.Predicate;
37
import java.util.function.Supplier;
38
import java.util.stream.Collectors;
39
import java.util.stream.Stream;
40
import org.creekservice.api.base.type.Suppliers;
41
import org.creekservice.api.system.test.extension.test.model.Expectation;
42
import org.creekservice.api.system.test.extension.test.model.Input;
43
import org.creekservice.api.system.test.extension.test.model.Ref;
44
import org.creekservice.api.system.test.model.TestCase;
45
import org.creekservice.api.system.test.model.TestCaseDef;
46
import org.creekservice.api.system.test.model.TestPackage;
47
import org.creekservice.api.system.test.model.TestSuite;
48
import org.creekservice.api.system.test.model.TestSuiteDef;
49
import org.creekservice.api.system.test.parser.ModelType;
50
import org.creekservice.api.system.test.parser.TestPackageParser;
51

52
/**
53
 * Parse test suites from a directory structure of Yaml files.
54
 *
55
 * <p>Expected directory structure:
56
 *
57
 * <pre>
58
 * root
59
 *   |--seed
60
 *   |--inputs
61
 *   |--expectations
62
 * </pre>
63
 *
64
 * <p>...with test suites defined in the root directory.
65
 */
66
public final class YamlTestPackageParser implements TestPackageParser {
67

68
    private static final PathMatcher YAML_MATCHER =
69
            FileSystems.getDefault().getPathMatcher("regex:.*\\.yml|.*\\.yaml");
1✔
70

71
    private static final Path SEED = Paths.get("seed");
1✔
72
    private static final Path INPUTS = Paths.get("inputs");
1✔
73
    private static final Path EXPECTATIONS = Paths.get("expectations");
1✔
74

75
    private final ObjectMapper mapper;
76
    private final Observer observer;
77

78
    /**
79
     * @param modelExtensions known model extensions
80
     * @param observer a parsing observer
81
     */
82
    public YamlTestPackageParser(
83
            final Collection<ModelType<?>> modelExtensions, final Observer observer) {
1✔
84
        this.mapper = SystemTestMapper.create(modelExtensions);
1✔
85
        this.observer = requireNonNull(observer, "observer");
1✔
86
    }
1✔
87

88
    @Override
89
    public Optional<TestPackage> parse(final Path path, final Predicate<Path> predicate) {
90
        if (!Files.isDirectory(path.resolve(EXPECTATIONS))) {
1✔
91
            // Test cases must have at least one expectation.
92
            // Therefore, no expectation dir means no test suites:
93
            return Optional.empty();
1✔
94
        }
95

96
        final List<Input> seedData =
1✔
97
                loadDir(path.resolve(SEED), Input.class)
1✔
98
                        .map(LazyFile::content)
1✔
99
                        .collect(Collectors.toList());
1✔
100

101
        final Map<String, LazyFile<Input>> inputs =
1✔
102
                loadDir(path.resolve(INPUTS), Input.class)
1✔
103
                        .collect(Collectors.toMap(LazyFile::id, Function.identity()));
1✔
104

105
        final Map<String, LazyFile<Expectation>> expectations =
1✔
106
                loadDir(path.resolve(EXPECTATIONS), Expectation.class)
1✔
107
                        .collect(Collectors.toMap(LazyFile::id, Function.identity()));
1✔
108

109
        final List<TestSuite.Builder> suites =
1✔
110
                loadDir(path, TestSuiteDef.class)
1✔
111
                        .filter(f -> predicate.test(f.path()))
1✔
112
                        .map(f -> testSuiteBuilder(f.content(), inputs, expectations))
1✔
113
                        .toList();
1✔
114

115
        if (suites.isEmpty()) {
1✔
116
            return Optional.empty();
1✔
117
        }
118

119
        warnOnUnusedDependencies(path, inputs, expectations);
1✔
120

121
        return Optional.of(testPackage(seedData, suites));
1✔
122
    }
123

124
    private <T> Stream<LazyFile<T>> loadDir(final Path dir, final Class<T> type) {
125
        return ymlFilesInDir(dir).stream()
1✔
126
                .map(path -> new LazyFile<>(id(path), path, () -> parse(path, type)));
1✔
127
    }
128

129
    private List<Path> ymlFilesInDir(final Path dir) {
130
        if (!Files.exists(dir)) {
1✔
131
            return List.of();
1✔
132
        }
133

134
        try (Stream<Path> stream = Files.walk(dir, 1)) {
1✔
135
            return stream.filter(Files::isRegularFile).filter(YAML_MATCHER::matches).toList();
1✔
UNCOV
136
        } catch (final IOException e) {
×
137
            throw new TestLoadFailedException("Error accessing directory " + dir, e);
×
138
        }
139
    }
140

141
    private <T> T parse(final Path path, final Class<T> type) {
142
        try {
143
            return mapper.readValue(path.toFile(), type);
1✔
144
        } catch (final Exception e) {
1✔
145
            throw new InvalidTestFileException(
1✔
146
                    "Failed to load "
147
                            + type.getSimpleName()
1✔
148
                            + " from "
149
                            + path.toUri()
1✔
150
                            + lineSeparator()
1✔
151
                            + "Please check the file is valid."
152
                            + lineSeparator()
1✔
153
                            + e.getMessage(),
1✔
154
                    e);
155
        }
156
    }
157

158
    private static TestSuite.Builder testSuiteBuilder(
159
            final TestSuiteDef def,
160
            final Map<String, LazyFile<Input>> inputs,
161
            final Map<String, LazyFile<Expectation>> expectations) {
162
        try {
163
            final List<TestCase.Builder> testCases =
1✔
164
                    def.tests().stream()
1✔
165
                            .map(testCaseDef -> testCaseBuilder(testCaseDef, inputs, expectations))
1✔
166
                            .collect(Collectors.toList());
1✔
167

168
            return TestSuite.testSuite(testCases, def);
1✔
169
        } catch (final InvalidTestFileException | MissingDependencyException e) {
1✔
170
            throw new InvalidTestFileException(
1✔
171
                    "Error in suite '" + def.name() + "':" + e.getMessage(), e);
1✔
172
        }
173
    }
174

175
    private static TestCase.Builder testCaseBuilder(
176
            final TestCaseDef def,
177
            final Map<String, LazyFile<Input>> inputs,
178
            final Map<String, LazyFile<Expectation>> expectations) {
179
        try {
180
            final List<Input> testInputs =
1✔
181
                    def.inputs().stream()
1✔
182
                            .map(i -> findDependency(i, inputs))
1✔
183
                            .collect(Collectors.toList());
1✔
184

185
            final List<Expectation> testExpectations =
1✔
186
                    def.expectations().stream()
1✔
187
                            .map(e -> findDependency(e, expectations))
1✔
188
                            .collect(Collectors.toList());
1✔
189

190
            return TestCase.testCase(testInputs, testExpectations, def);
1✔
191
        } catch (final InvalidTestFileException | MissingDependencyException e) {
1✔
192
            throw new InvalidTestFileException("'" + def.name() + "': " + e.getMessage(), e);
1✔
193
        }
194
    }
195

196
    private void warnOnUnusedDependencies(
197
            final Path path,
198
            final Map<String, LazyFile<Input>> inputs,
199
            final Map<String, LazyFile<Expectation>> expectations) {
200
        final List<Path> unused =
1✔
201
                Stream.concat(inputs.values().stream(), expectations.values().stream())
1✔
202
                        .filter(LazyFile::unused)
1✔
203
                        .map(LazyFile::path)
1✔
204
                        .map(Path::toAbsolutePath)
1✔
205
                        .toList();
1✔
206

207
        if (!unused.isEmpty()) {
1✔
208
            observer.unusedDependencies(path, unused);
1✔
209
        }
210
    }
1✔
211

212
    private static <T> T findDependency(final Ref ref, final Map<String, LazyFile<T>> known) {
213
        final LazyFile<T> dependency = known.get(ref.id());
1✔
214
        if (dependency == null) {
1✔
215
            throw new MissingDependencyException(ref);
1✔
216
        }
217
        return dependency.content();
1✔
218
    }
219

220
    @SuppressFBWarnings(
221
            value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
222
            justification = "path always has at least one part and extension")
223
    private static String id(final Path path) {
224
        final String fileName = path.getFileName().toString();
1✔
225
        return fileName.substring(0, fileName.lastIndexOf('.'));
1✔
226
    }
227

228
    private static final class LazyFile<T> {
229

230
        private final String id;
231
        private final Path path;
232
        private final Supplier<T> content;
233
        private boolean unused = true;
1✔
234

235
        LazyFile(final String id, final Path path, final Supplier<T> content) {
1✔
236
            this.id = requireNonNull(id, "id");
1✔
237
            this.path = requireNonNull(path, "path");
1✔
238
            this.content = Suppliers.memoize(content);
1✔
239
        }
1✔
240

241
        String id() {
242
            return id;
1✔
243
        }
244

245
        Path path() {
246
            return path;
1✔
247
        }
248

249
        T content() {
250
            unused = false;
1✔
251
            return content.get();
1✔
252
        }
253

254
        boolean unused() {
255
            return unused;
1✔
256
        }
257
    }
258
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc