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

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

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

74.81
/exist-core/src/main/java/org/exist/test/runner/XSuite.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 */
24
package org.exist.test.runner;
25

26
import org.exist.test.ExistEmbeddedServer;
27
import org.exist.util.XMLFilenameFilter;
28
import org.exist.util.XQueryFilenameFilter;
29
import org.junit.AfterClass;
30
import org.junit.BeforeClass;
31
import org.junit.runner.Description;
32
import org.junit.runner.Runner;
33
import org.junit.runner.notification.RunNotifier;
34
import org.junit.runners.ParentRunner;
35
import org.junit.runners.model.*;
36

37
import javax.annotation.Nullable;
38
import java.io.IOException;
39
import java.lang.annotation.*;
40
import java.nio.file.Files;
41
import java.nio.file.Path;
42
import java.nio.file.Paths;
43
import java.util.ArrayList;
44
import java.util.Collections;
45
import java.util.List;
46
import java.util.stream.Collectors;
47
import java.util.stream.Stream;
48

49
/**
50
 * Using <code>XSuite</code> as a runner allows you to manually
51
 * build a suite containing tests from both:
52
 *
53
 * 1. XQSuite - as defined in $EXIST_HOME/src/org/exist/xquery/lib/xqsuite/xqsuite.xql
54
 * 2. XML Test - as defined in $EXIST_HOME/src/org/exist/xquery/lib/test.xq
55
 *
56
 * To use it, annotate a class
57
 * with <code>@RunWith(XSuite.class)</code> and <code>@XSuiteClasses({"extensions/my-extension/src/test/xquery", ...})</code>.
58
 * When you run this class, it will run all the tests in all the suite classes.
59
 *
60
 * @author Adam Retter
61
 */
62
public class XSuite extends ParentRunner<Runner> {
63

64
    /**
65
     * Returns an empty suite.
66
     *
67
     * @return a runner for an empty suite.
68
     */
69
    public static Runner emptySuite() {
70
        try {
71
            return new XSuite((Class<?>) null, new String[0]);
×
72
        } catch (InitializationError e) {
×
73
            throw new RuntimeException("This shouldn't be possible");
×
74
        }
75
    }
76

77
    /**
78
     * The <code>XSuiteFiles</code> annotation specifies the directory/file containing the tests to be run when a class
79
     * annotated with <code>@RunWith(XSuite.class)</code> is run.
80
     */
81
    @Retention(RetentionPolicy.RUNTIME)
82
    @Target(ElementType.TYPE)
83
    @Inherited
84
    public @interface XSuiteFiles {
85
        /**
86
         * @return the classes to be run
87
         */
88
        String[] value();
89
    }
90

91
    /**
92
     * The <code>XSuiteParallel</code> annotation specifies that the tests will be run in parallel when a class
93
     * annotated with <code>@RunWith(XSuite.class)</code> is run.
94
     */
95
    @Retention(RetentionPolicy.RUNTIME)
96
    @Target(ElementType.TYPE)
97
    @Inherited
98
    public @interface XSuiteParallel {
99
    }
100

101
    private static String[] getAnnotatedDirectories(final Class<?> klass) throws InitializationError {
102
        final XSuite.XSuiteFiles annotation = klass.getAnnotation(XSuite.XSuiteFiles.class);
1✔
103
        if (annotation == null) {
1!
104
            throw new InitializationError(String.format("class '%s' must have a XSuiteFiles annotation", klass.getName()));
×
105
        }
106
        return annotation.value();
1✔
107
    }
108

109
    private static boolean hasParallelAnnotation(@Nullable final Class<?> klass) {
110
        if (klass == null) {
1!
111
            return false;
×
112
        }
113
        final XSuite.XSuiteParallel annotation = klass.getAnnotation(XSuite.XSuiteParallel.class);
1✔
114
        return annotation != null;
1!
115
    }
116

117
    private final List<Runner> runners;
118

119
    /**
120
     * Called reflectively on classes annotated with <code>@RunWith(XSuite.class)</code>
121
     *
122
     * @param klass the root class
123
     * @param builder builds runners for classes in the suite
124
     *
125
     * @throws InitializationError if the XSuite cannot be constructed
126
     */
127
    public XSuite(final Class<?> klass, final RunnerBuilder builder) throws InitializationError {
128
        this(builder, klass, getAnnotatedDirectories(klass));
1✔
129
    }
1✔
130

131
    /**
132
     * Call this when there is no single root class (for example, multiple class names
133
     * passed on the command line to {@link org.junit.runner.JUnitCore}
134
     *
135
     * @param builder builds runners for classes in the suite
136
     * @param suites the directories/files in the suite
137
     *
138
     * @throws InitializationError if the XSuite cannot be constructed
139
     */
140
    public XSuite(final RunnerBuilder builder, final String[] suites) throws InitializationError {
141
        this(builder, null, suites);
×
142
    }
×
143

144
    /**
145
     * Call this when the default builder is good enough. Left in for compatibility with JUnit 4.4.
146
     *
147
     * @param klass the root of the suite
148
     * @param suites the directories/files in the suite
149
     *
150
     * @throws InitializationError if the XSuite cannot be constructed
151
     */
152
    protected XSuite(final Class<?> klass, final String[] suites) throws InitializationError {
153
        this(null, klass, suites);
×
154
    }
×
155

156
    /**
157
     * Called by this class and subclasses once the classes making up the suite have been determined.
158
     *
159
     * @param builder builds runners for classes in the suite
160
     * @param klass the root of the suite
161
     * @param suites the directories/files in the suite
162
     *
163
     * @throws InitializationError if the XSuite cannot be constructed
164
     */
165
    protected XSuite(final RunnerBuilder builder, final Class<?> klass, final String[] suites) throws InitializationError {
166
        this(klass, getRunners(suites, hasParallelAnnotation(klass)));
1✔
167
    }
1✔
168

169
    /**
170
     * Called by this class and subclasses once the runners making up the suite have been determined.
171
     *
172
     * @param klass root of the suite
173
     * @param runners for each class in the suite, a {@link Runner}
174
     *
175
     * @throws InitializationError if the XSuite cannot be constructed
176
     */
177
    protected XSuite(final Class<?> klass, final List<Runner> runners) throws InitializationError {
178
        super(klass);
1✔
179
        this.runners = Collections.unmodifiableList(runners);
1✔
180
    }
1✔
181

182
    /**
183
     * Get the runners for the suiteDirectories.
184
     *
185
     * @param suites/files the directories in the suite
186
     * @param parallel should a runner execute tests in parallel
187
     *
188
     * @throws InitializationError if the runners cannot be retrieved
189
     */
190
    private static List<Runner> getRunners(final String[] suites, final boolean parallel) throws InitializationError {
191
        if(suites == null) {
1!
192
            return Collections.emptyList();
×
193
        }
194

195
        try {
196

197
            final List<Runner> runners = new ArrayList<>();
1✔
198
            for (final String suite : suites) {
1✔
199

200
                // if directory/file does not exist - throw an exception
201
                final Path path = Paths.get(suite);
1✔
202
                if (!Files.exists(path)) {
1!
203
                    throw new InitializationError("XSuite does not exist: " + suite + ". path=" + path.toAbsolutePath().toString());
×
204
                }
205

206
                if (Files.isDirectory(path)) {
1!
207
                    // directory of files of test(s)
208
                    try (final Stream<Path> children = Files.list(path)) {
1✔
209
                        for(final Path child : children.collect(Collectors.toList())) {
1✔
210
                            if(!Files.isDirectory(child)) {
1✔
211
                                final Runner runner = getRunner(child, parallel);
1✔
212
                                if(runner != null) {
1✔
213
                                    runners.add(runner);
1✔
214
                                }
215
                            }
216
                        }
217
                    }
218
                } else {
219
                    // just a file of test(s)
220
                    runners.add(getRunner(path, parallel));
×
221
                }
222
            }
223

224
            return runners;
1✔
225
        } catch (final IOException e) {
×
226
            throw new InitializationError(e);
×
227
        }
228
    }
229

230
    private static @Nullable Runner getRunner(final Path path, final boolean parallel) throws InitializationError {
231
        if(XMLFilenameFilter.asPredicate().test(path)) {
1✔
232
            return new XMLTestRunner(path, parallel);
1✔
233
        } else if(XQueryFilenameFilter.asPredicate().test(path) && !path.getFileName().toString().equals("runTests.xql")) {
1!
234
            return new XQueryTestRunner(path, parallel);
1✔
235
        } else {
236
            return null;
1✔
237
        }
238
    }
239

240
    @Override
241
    protected List<Runner> getChildren() {
242
        return runners;
1✔
243
    }
244

245
    @Override
246
    protected Description describeChild(final Runner child) {
247
        return child.getDescription();
1✔
248
    }
249

250
    @Override
251
    protected void runChild(final Runner runner, final RunNotifier notifier) {
252
        runner.run(notifier);
1✔
253
    }
1✔
254

255
    @Override
256
    protected Statement withBeforeClasses(final Statement statement) {
257
        // get @BeforeClass methods
258
        final List<FrameworkMethod> befores = getTestClass().getAnnotatedMethods(BeforeClass.class);
1✔
259

260
        // inject a Server startup as though it were an @BeforeClass
261
        final Statement startExistDb = new StartExistDbStatement();
1✔
262

263
        return new RunXSuiteBefores(statement, startExistDb, befores, null);
1✔
264
    }
265

266
    @Override
267
    protected Statement withAfterClasses(final Statement statement) {
268
        // get @AfterClass methods
269
        final List<FrameworkMethod> afters = getTestClass().getAnnotatedMethods(AfterClass.class);
1✔
270

271
        // inject a Server shutdown as though it were an @AfterClass
272
        final Statement stopExist = new StopExistDbStatement();
1✔
273

274
        return new RunXSuiteAfters(statement, stopExist, afters, null);
1✔
275
    }
276

277
    private static class RunXSuiteBefores extends Statement {
278
        private final Statement next;
279
        private final Object target;
280
        private final Statement beforeXSuite;
281
        private final List<FrameworkMethod> befores;
282

283
        public RunXSuiteBefores(final Statement next, final Statement beforeXSuite, final List<FrameworkMethod> befores, final Object target) {
1✔
284
            this.next = next;
1✔
285
            this.beforeXSuite = beforeXSuite;
1✔
286
            this.befores = befores;
1✔
287
            this.target = target;
1✔
288
        }
1✔
289

290
        @Override
291
        public void evaluate() throws Throwable {
292
            beforeXSuite.evaluate();
1✔
293
            for (final FrameworkMethod before : befores) {
1!
294
                before.invokeExplosively(target);
×
295
            }
296
            next.evaluate();
1✔
297
        }
1✔
298
    }
299

300
    private static class RunXSuiteAfters extends Statement {
301
        private final Statement next;
302
        private final Object target;
303
        private final Statement afterXSuite;
304
        private final List<FrameworkMethod> afters;
305

306
        public RunXSuiteAfters(final Statement next, final Statement afterXSuite, final List<FrameworkMethod> afters, final Object target) {
1✔
307
            this.next = next;
1✔
308
            this.afterXSuite = afterXSuite;
1✔
309
            this.afters = afters;
1✔
310
            this.target = target;
1✔
311
        }
1✔
312

313
        @Override
314
        public void evaluate() throws Throwable {
315
            final List<Throwable> errors = new ArrayList<>();
1✔
316
            try {
317
                next.evaluate();
1✔
318
            } catch (final Throwable e) {
1✔
319
                errors.add(e);
×
320
            } finally {
321
                try {
322
                    afterXSuite.evaluate();
1✔
323
                } catch (final Throwable e) {
1✔
324
                    errors.add(e);
×
325
                }
326
                for (final FrameworkMethod after : afters) {
1!
327
                    try {
328
                        after.invokeExplosively(target);
×
329
                    } catch (final Throwable e) {
×
330
                        errors.add(e);
×
331
                    }
332
                }
333
            }
334
            MultipleFailureException.assertEmpty(errors);
1✔
335
        }
1✔
336
    }
337

338
    private static class StartExistDbStatement extends Statement {
339
        @Override
340
        public void evaluate() throws Throwable {
341
            if (EXIST_EMBEDDED_SERVER_CLASS_INSTANCE != null) {
1!
342
                throw new IllegalStateException("EXIST_EMBEDDED_SERVER_CLASS_INSTANCE already instantiated");
×
343
            }
344
            EXIST_EMBEDDED_SERVER_CLASS_INSTANCE = newExistDbServer();
1✔
345
            EXIST_EMBEDDED_SERVER_CLASS_INSTANCE.startDb();
1✔
346
        }
1✔
347
    }
348

349
    private static class StopExistDbStatement extends Statement {
350
        @Override
351
        public void evaluate() {
352
            if (EXIST_EMBEDDED_SERVER_CLASS_INSTANCE == null) {
1!
353
                throw new IllegalStateException("EXIST_EMBEDDED_SERVER_CLASS_INSTANCE already stopped");
×
354
            }
355
            EXIST_EMBEDDED_SERVER_CLASS_INSTANCE.stopDb();
1✔
356
            EXIST_EMBEDDED_SERVER_CLASS_INSTANCE = null;
1✔
357
        }
1✔
358
    }
359

360
    static ExistEmbeddedServer EXIST_EMBEDDED_SERVER_CLASS_INSTANCE = null;
1✔
361

362
    static ExistEmbeddedServer newExistDbServer() {
363
        return new ExistEmbeddedServer(true, true);
1✔
364
    }
365
}
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

© 2025 Coveralls, Inc