001/**
002 *
003 * Copyright 2019 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jxmpp.strings.testframework;
018
019import java.io.IOException;
020import java.time.ZonedDateTime;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.List;
025import java.util.concurrent.ExecutorService;
026import java.util.concurrent.Executors;
027import java.util.concurrent.Phaser;
028import java.util.function.BiConsumer;
029import java.util.function.Function;
030import java.util.stream.Collectors;
031
032import org.jxmpp.jid.Jid;
033import org.jxmpp.jid.impl.JidCreate;
034import org.jxmpp.stringprep.XmppStringprepException;
035import org.parboiled.common.FileUtils;
036
037public class StringsTestframework {
038
039        private static final ExecutorService EXECUTOR = Executors
040                        .newFixedThreadPool(Runtime.getRuntime().availableProcessors());
041
042        private final Configuration configuration;
043
044        private final Phaser phaser = new Phaser(0);
045
046        /**
047         * Construct a new instance of the test framework with the default configuration.
048         */
049        public StringsTestframework() {
050                this(Configuration.builder().withAllKnownXmppStringpreppers().build());
051        }
052
053        /**
054         * Construct a new instance of the test framework with the given configuration.
055         *
056         * @param configuration the configuration to use.
057         */
058        public StringsTestframework(Configuration configuration) {
059                this.configuration = configuration;
060        }
061
062        /**
063         * Run the test framework.
064         * 
065         * @return <code>true</code> if there where no errors.
066         */
067        public synchronized Result runTests() {
068                phaser.register();
069
070                List<ValidJid> validJids = parseValidJids();
071                List<InvalidJid> invalidJids = parseInvalidJids();
072
073
074                List<XmppStringPrepperState> xmppStringPreppersState = configuration.xmppStringPreppers
075                                .stream()
076                                .map(s -> new XmppStringPrepperState(s, validJids.size(), invalidJids.size()))
077                                .collect(Collectors.toList());
078
079                executeForEach(xmppStringPreppersState, validJids, StringsTestframework::testValidJids);
080
081                executeForEach(xmppStringPreppersState, invalidJids, StringsTestframework::testInvalidJids);
082
083                phaser.arriveAndAwaitAdvance();
084
085                return new Result(xmppStringPreppersState);
086        }
087
088        private <T, U> void executeForEach(List<? extends T> smallList, Collection<? extends U> bigCollection,
089                        BiConsumer<T, U> biConsumer) {
090                // Create a childPhaser for every item in smallList.
091                List<Phaser> childPhasers = smallList.stream()
092                                .map(__ -> new Phaser(phaser, bigCollection.size() + 1))
093                                .collect(Collectors.toList());
094
095                for (U u : bigCollection) {
096                        for (int i = 0; i < smallList.size(); i++) {
097                                T t = smallList.get(i);
098                                Phaser phaser = childPhasers.get(i);
099
100                                EXECUTOR.execute(() -> {
101                                        try {
102                                                biConsumer.accept(t, u);
103                                        } finally {
104                                                phaser.arrive();
105                                        }
106                                });
107                        }
108                }
109
110                childPhasers.forEach(p -> p.arrive());
111        }
112
113        private static void testValidJids(XmppStringPrepperState xmppStringPrepperState, ValidJid validJid) {
114                XmppStringPrepper xmppStringPrepper = xmppStringPrepperState.xmppStringPrepper;
115                Jid jid;
116
117                long startNanos = System.nanoTime();
118                try {
119                        jid = JidCreate.from(validJid.unnormalizedJid, xmppStringPrepper.context);
120                } catch (XmppStringprepException e) {
121                        long stopNanos = System.nanoTime();
122                        ValidJidTestresult.FailedBecauseException failed = new ValidJidTestresult.FailedBecauseException(
123                                        xmppStringPrepper, startNanos, stopNanos, validJid, e);
124                        xmppStringPrepperState.validJidFailedTestresults.add(failed);
125                        return;
126                }
127                long stopNanos = System.nanoTime();
128
129                boolean domainpartMismatch = false, localpartMismatch = false, resourcepartMismatch = false;
130
131                String domainpart = jid.getDomain().toString();
132                if (!domainpart.equals(validJid.domainpart)) {
133                        domainpartMismatch = true;
134                }
135
136                if (!validJid.localpart.isEmpty()) {
137                        if (!jid.hasLocalpart()) {
138                                localpartMismatch = true;
139                        } else {
140                                String localpart = jid.getLocalpartOrThrow().toString();
141                                if (!localpart.equals(validJid.localpart)) {
142                                        localpartMismatch = true;
143                                }
144                        }
145                }
146
147                if (!validJid.resourcepart.isEmpty()) {
148                        if (jid.hasNoResource()) {
149                                resourcepartMismatch = true;
150                        } else {
151                                String resourcepart = jid.getResourceOrThrow().toString();
152                                if (!resourcepart.equals(validJid.resourcepart)) {
153                                        resourcepartMismatch = true;
154                                }
155                        }
156                }
157
158                if (domainpartMismatch || localpartMismatch || resourcepartMismatch) {
159                        ValidJidTestresult.FailedBecauseMismatch failed = new ValidJidTestresult.FailedBecauseMismatch(
160                                        xmppStringPrepper, startNanos, stopNanos, validJid, jid, domainpartMismatch, localpartMismatch,
161                                        resourcepartMismatch);
162                        xmppStringPrepperState.validJidFailedTestresults.add(failed);
163                } else {
164                        ValidJidTestresult.Successful successful = new ValidJidTestresult.Successful(xmppStringPrepper, startNanos,
165                                        stopNanos, validJid, jid);
166                        xmppStringPrepperState.validJidSuccessfulTestresults.add(successful);
167                }
168        }
169
170        private static void testInvalidJids(XmppStringPrepperState xmppStringPrepperState, InvalidJid invalidJid) {
171                XmppStringPrepper xmppStringPrepper = xmppStringPrepperState.xmppStringPrepper;
172                Jid jid;
173
174                long startNanos = System.nanoTime();
175                try {
176                        jid = JidCreate.from(invalidJid.invalidJid, xmppStringPrepper.context);
177                } catch (XmppStringprepException e) {
178                        long stopNanos = System.nanoTime();
179                        InvalidJidTestresult.Successful successful = new InvalidJidTestresult.Successful(xmppStringPrepper,
180                                        startNanos, stopNanos, invalidJid, e);
181                        xmppStringPrepperState.invalidJidSuccessfulTestresults.add(successful);
182                        return;
183                }
184                long stopNanos = System.nanoTime();
185
186                InvalidJidTestresult.Failed failed = new InvalidJidTestresult.Failed(xmppStringPrepper, startNanos, stopNanos,
187                                invalidJid, jid);
188                xmppStringPrepperState.invalidJidFailedTestresults.add(failed);
189        }
190
191        static List<ValidJid> parseValidJids() {
192                String mainValidJids = FileUtils.readAllTextFromResource("xmpp-strings/jids/valid/main");
193                ValidJidCorpusParser parser = new ValidJidCorpusParser(mainValidJids);
194                List<ValidJid> validJids = parser.parse();
195                return validJids;
196        }
197
198        static List<InvalidJid> parseInvalidJids() {
199                String mainInvalidJids = FileUtils.readAllTextFromResource("xmpp-strings/jids/invalid/main");
200                InvalidJidCorpusParser parser = new InvalidJidCorpusParser(mainInvalidJids);
201                List<InvalidJid> invalidJids = parser.parse();
202                return invalidJids;
203        }
204
205        private static class XmppStringPrepperState {
206
207                private final XmppStringPrepper xmppStringPrepper;
208
209                private final List<ValidJidTestresult.Successful> validJidSuccessfulTestresults;
210                private final List<ValidJidTestresult.Failed> validJidFailedTestresults;
211
212                private final List<InvalidJidTestresult.Successful> invalidJidSuccessfulTestresults;
213                private final List<InvalidJidTestresult.Failed> invalidJidFailedTestresults;
214
215                private XmppStringPrepperState(XmppStringPrepper xmppStringPrepper, int validJidCorpusSize,
216                                int invalidJidCorpusSize) {
217                        this.xmppStringPrepper = xmppStringPrepper;
218
219                        this.validJidSuccessfulTestresults = Collections.synchronizedList(new ArrayList<>(validJidCorpusSize));
220                        this.validJidFailedTestresults = Collections.synchronizedList(new ArrayList<>());
221
222                        this.invalidJidSuccessfulTestresults = Collections.synchronizedList(new ArrayList<>(invalidJidCorpusSize));
223                        this.invalidJidFailedTestresults = Collections.synchronizedList(new ArrayList<>());
224                }
225
226        }
227
228        public static class Configuration {
229                final List<XmppStringPrepper> xmppStringPreppers;
230
231                private Configuration(Builder builder) {
232                        xmppStringPreppers = Collections.unmodifiableList(new ArrayList<>(builder.xmppStringPreppers));
233                }
234
235                /**
236                 * Construct a new builder.
237                 *
238                 * @return a newly constructed builder.
239                 */
240                public static Builder builder() {
241                        return new Builder();
242                }
243
244                public static final class Builder {
245                        List<String> validJidCorpusResources = new ArrayList<>();
246                        List<XmppStringPrepper> xmppStringPreppers = new ArrayList<>();
247
248                        private Builder() {
249                        }
250
251                        /**
252                         * Use the given XMPP String Prepper with the given name.
253                         *
254                         * @param xmppStringPrepperName the name of the XMPP String Prepper.
255                         * @return an reference to this builder instance.
256                         */
257                        public Builder withXmppStringPrepper(String xmppStringPrepperName) {
258                                XmppStringPrepper xmppStringPrepper = XmppStringPrepper.lookup(xmppStringPrepperName);
259                                if (xmppStringPrepper == null) {
260                                        throw new IllegalArgumentException(
261                                                        "No XMPP stringprepper with the name '" + xmppStringPrepperName + "' known.");
262                                }
263
264                                xmppStringPreppers.add(xmppStringPrepper);
265                                return this;
266                        }
267
268                        /**
269                         * Use all known XMPP String Preppers.
270                         *
271                         * @return an reference to this builder instance.
272                         */
273                        public Builder withAllKnownXmppStringpreppers() {
274                                List<XmppStringPrepper> xmppStringPreppers = XmppStringPrepper.getKnownXmppStringpreppers();
275                                this.xmppStringPreppers.addAll(xmppStringPreppers);
276                                return this;
277                        }
278
279                        /**
280                         * Add the given collection of XMPP String Preppers to the list of used preppers.
281                         *
282                         * @param xmppStringPreppers a collection of XMPP String Preppers.
283                         * @return an reference to this builder instance.
284                         */
285                        public Builder addXmppStringPreppers(Collection<? extends XmppStringPrepper> xmppStringPreppers) {
286                                this.xmppStringPreppers.addAll(xmppStringPreppers);
287                                return this;
288                        }
289
290                        /**
291                         * Build the configuration.
292                         *
293                         * @return the build configuration.
294                         */
295                        public Configuration build() {
296                                return new Configuration(this);
297                        }
298                }
299        }
300
301        public static class XmppStringPrepperResult implements Comparable<XmppStringPrepperResult> {
302
303                public final XmppStringPrepper xmppStringPrepper;
304
305                public final List<ValidJidTestresult.Successful> validJidSuccessfulTestresults;
306                public final List<ValidJidTestresult.Failed> validJidFailedTestresults;
307
308                public final List<InvalidJidTestresult.Successful> invalidJidSuccessfulTestresults;
309                public final List<InvalidJidTestresult.Failed> invalidJidFailedTestresults;
310
311                public final int totalSuccessful, totalFailed;
312
313                private XmppStringPrepperResult(XmppStringPrepperState xmppStringPrepperState) {
314                        xmppStringPrepper = xmppStringPrepperState.xmppStringPrepper;
315
316                        validJidSuccessfulTestresults = Collections
317                                        .unmodifiableList(new ArrayList<>(xmppStringPrepperState.validJidSuccessfulTestresults));
318                        validJidFailedTestresults = Collections
319                                        .unmodifiableList(new ArrayList<>(xmppStringPrepperState.validJidFailedTestresults));
320
321                        invalidJidSuccessfulTestresults = Collections
322                                        .unmodifiableList(new ArrayList<>(xmppStringPrepperState.invalidJidSuccessfulTestresults));
323                        invalidJidFailedTestresults = Collections
324                                        .unmodifiableList(new ArrayList<>(xmppStringPrepperState.invalidJidFailedTestresults));
325
326                        totalSuccessful = validJidSuccessfulTestresults.size() + invalidJidSuccessfulTestresults.size();
327                        totalFailed = validJidFailedTestresults.size() + invalidJidFailedTestresults.size();
328                }
329
330                @Override
331                public int compareTo(XmppStringPrepperResult o) {
332                        if (totalSuccessful != o.totalSuccessful) {
333                                return Integer.compare(totalSuccessful, o.totalSuccessful);
334                        }
335
336                        int myValidJidSuccessfulCount = validJidFailedTestresults.size();
337                        int otherValidJidSuccessfulCount = o.validJidSuccessfulTestresults.size();
338                        if (myValidJidSuccessfulCount != otherValidJidSuccessfulCount) {
339                                return Integer.compare(myValidJidSuccessfulCount, otherValidJidSuccessfulCount);
340                        }
341
342                        return 0;
343                }
344
345        }
346
347        public static class Result {
348                public final List<XmppStringPrepperResult> xmppStringPrepperResults;
349
350                public final List<ValidJidTestresult.Successful> validJidSuccessfulTestresults;
351                public final List<ValidJidTestresult.Failed> validJidFailedTestresults;
352
353                public final List<InvalidJidTestresult.Successful> invalidJidSuccessfulTestresults;
354                public final List<InvalidJidTestresult.Failed> invalidJidFailedTestresults;
355
356                public final boolean wasSuccessful;
357                public final int totalSuccessful;
358                public final int totalFailed;
359
360                public final ZonedDateTime time = ZonedDateTime.now();
361
362                Result(List<XmppStringPrepperState> xmppStringPreppersState) {
363                        xmppStringPrepperResults = xmppStringPreppersState.stream()
364                                        .map(s -> new XmppStringPrepperResult(s))
365                                        .collect(Collectors.collectingAndThen(Collectors.toList(),
366                                                        results -> {
367                                                                Collections.sort(results);
368                                                                return Collections.unmodifiableList(results);
369                                                        }));
370
371                        validJidSuccessfulTestresults = gather(xmppStringPrepperResults, s -> s.validJidSuccessfulTestresults);
372                        validJidFailedTestresults = gather(xmppStringPrepperResults, s -> s.validJidFailedTestresults);
373
374                        invalidJidSuccessfulTestresults = gather(xmppStringPrepperResults, s -> s.invalidJidSuccessfulTestresults);
375                        invalidJidFailedTestresults = gather(xmppStringPrepperResults, s -> s.invalidJidFailedTestresults);
376
377                        totalSuccessful = validJidSuccessfulTestresults.size() + invalidJidSuccessfulTestresults.size();
378                        totalFailed = validJidFailedTestresults.size() + invalidJidFailedTestresults.size();
379                        wasSuccessful = totalFailed == 0;
380                }
381
382                private static <I,R> List<R> gather(Collection<I> inputCollection,
383                                Function<? super I, Collection<? extends R>> mapper) {
384                        return inputCollection.stream()
385                                        .map(mapper)
386                                        .flatMap(l -> l.stream())
387                                        // TODO Use Collectors.toUnmodifiableList() once jXMPP is on Java 10 or higher.
388                                        .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
389                }
390
391                /**
392                 * Write a report of the results to the given appendable.
393                 *
394                 * @param appendable the appendable to write to.
395                 * @throws IOException any IOExceptions while writing.
396                 */
397                public void writeResults(Appendable appendable) throws IOException {
398                        appendable.append(
399                                        "jXMPP Strings Testframework Result\n" +
400                                        "===================================\n" +
401                                        "Successful Tests: " + totalSuccessful + '\n' +
402                                        "Failed Tests    : " + totalFailed + '\n'
403                                        );
404                        if (!wasSuccessful) {
405                                appendable.append(
406                                                "Some tests FAILED! :(\n");
407                                for (ValidJidTestresult.Failed failed : validJidFailedTestresults) {
408                                        appendable.append(failed.toString()).append('\n');
409                                }
410                                for (InvalidJidTestresult.Failed failed : invalidJidFailedTestresults) {
411                                        appendable.append(failed.toString()).append('\n');
412                                }
413                        }
414                }
415
416                @Override
417                public String toString() {
418                        StringBuilder sb = new StringBuilder();
419                        try {
420                                writeResults(sb);
421                        } catch (IOException e) {
422                                throw new AssertionError(e);
423                        }
424                        return sb.toString();
425                }
426        }
427}