All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.jqwik.engine.hooks.statistics.StatisticsCollectorImpl Maven / Gradle / Ivy

There is a newer version: 1.9.1
Show newest version
package net.jqwik.engine.hooks.statistics;

import java.util.*;
import java.util.function.*;
import java.util.regex.*;
import java.util.stream.*;

import org.opentest4j.*;

import net.jqwik.api.*;
import net.jqwik.api.statistics.*;
import net.jqwik.api.statistics.StatisticsCoverage.*;
import net.jqwik.engine.facades.*;

public class StatisticsCollectorImpl implements StatisticsCollector {
	public static final Object COLLECTORS_ID = Tuple.of(StatisticsCollectorImpl.class, "collectors");

	private final Map, Integer> counts = new LinkedHashMap<>();
	private final List> coverageCheckers = new ArrayList<>();
	private final String label;

	private List statisticsEntries = null;

	public StatisticsCollectorImpl(String label) {
		this.label = label;
	}

	@Override
	public StatisticsCollector collect(Object... values) {
		ensureAtLeastOneParameter(values);
		List key = keyFrom(values);
		ensureSameNumberOfValues(key);
		updateCounts(key);
		return this;
	}

	private void updateCounts(List key) {
		int count = counts.computeIfAbsent(key, any -> 0);
		counts.put(key, ++count);
		statisticsEntries = null;
	}

	private void ensureAtLeastOneParameter(Object[] values) {
		if (Arrays.equals(values, new Object[0])) {
			String message = String.format("StatisticsCollector[%s] must be called with at least one value", label);
			throw new IllegalArgumentException(message);
		}
	}

	private void ensureSameNumberOfValues(List keyCandidate) {
		if (counts.isEmpty()) {
			return;
		}
		List anyKey = counts.keySet().iterator().next();
		if (anyKey.size() != keyCandidate.size()) {
			String message = String.format("StatisticsCollector[%s] must always be called with same number of values", label);
			throw new IllegalArgumentException(message);
		}
	}

	private List keyFrom(Object[] values) {
		if (values != null) {
			return Arrays.asList(values);
		} else {
			return Collections.singletonList(null);
		}
	}

	// Currently only used for testing
	public double percentage(Object... values) {
		return statisticsEntry(values).percentage();
	}

	private StatisticsEntry statisticsEntry(Object[] values) {
		List key = keyFrom(values);
		return statisticsEntries()
				   .stream()
				   .filter(entry -> entry.values().equals(key))
				   .findFirst()
				   .orElse(StatisticsEntryImpl.nullFor(key));
	}

	private StatisticsEntry query(Predicate> query, int countAll) {
		return statisticsEntries()
				   .stream()
				   .filter(entry -> {
					   List values = entry.values();
					   return query.test(values);
				   })
				   .reduce(
					   StatisticsEntryImpl.nullWithName(""),
					   (statisticsEntry, other) -> statisticsEntry.plus(other, countAll)
				   );
	}

	private StatisticsEntryImpl matchPattern(String regex, int countAll) {
		String name = String.format("", regex);
		return statisticsEntries()
				   .stream()
				   .filter(entry -> {
					   if (entry.values().size() != 1) {
						   return false;
					   }
					   Object value = entry.values().get(0);
					   if (!(value instanceof CharSequence)) {
						   return false;
					   }
					   CharSequence toMatch = (CharSequence) value;
					   return Pattern.matches(regex, toMatch);
				   })
				   .reduce(
					   StatisticsEntryImpl.nullWithName(name),
					   (statisticsEntry, other) -> statisticsEntry.plus(other, countAll)
				   );
	}

	public int countAllCollects() {
		return counts.values().stream().mapToInt(aCount -> aCount).sum();
	}

	// Currently only used for testing
	public int count(Object... values) {
		return statisticsEntry(values).count();
	}

	@Override
	public void coverage(Consumer checker) {
		// The same checker shall only be used once
		if (!coverageCheckers.contains(checker)) {
			coverageCheckers.add(checker);
		}
	}

	public void checkCoverage() {
		for (Consumer checker : coverageCheckers) {
			StatisticsCoverage coverage = new StatisticsCoverageImpl();
			checker.accept(coverage);
		}
	}

	public Map, Integer> getCounts() {
		return counts;
	}

	public List statisticsEntries() {
		if (statisticsEntries != null) {
			return statisticsEntries;
		}
		statisticsEntries = calculateStatistics();
		return statisticsEntries;
	}

	private List calculateStatistics() {
		int sum = countAllCollects();
		return counts.entrySet()
					 .stream()
					 .sorted(this::compareStatisticsEntries)
					 .filter(entry -> !entry.getKey().equals(Collections.emptyList()))
					 .map(entry -> {
						 double percentage = entry.getValue() * 100.0 / sum;
						 return new StatisticsEntryImpl(
							 entry.getKey(),
							 displayKey(entry.getKey()),
							 entry.getValue(), percentage
						 );
					 })
					 .collect(Collectors.toList());
	}

	private int compareStatisticsEntries(Map.Entry, Integer> e1, Map.Entry, Integer> e2) {
		List k1 = e1.getKey();
		List k2 = e2.getKey();
		if (k1.size() != k2.size()) {
			return Integer.compare(k1.size(), k2.size());
		}
		return e2.getValue().compareTo(e1.getValue());
	}

	private String displayKey(List key) {
		return key.stream().map(Objects::toString).collect(Collectors.joining(" "));
	}

	String label() {
		return label;
	}

	private static String statisticsLabel(String label) {
		return label.equals(StatisticsFacadeImpl.DEFAULT_LABEL) ? "" : String.format(" for label \"%s\"", label);
	}

	private class StatisticsCoverageImpl implements StatisticsCoverage {

		@Override
		public CoverageChecker check(Object... values) {
			StatisticsEntry entry = statisticsEntry(values);
			return new CoverageCheckerImpl(label, entry, countAllCollects());
		}

		@SuppressWarnings("unchecked")
		@Override
		public CoverageChecker checkQuery(Predicate> query) {
			int countAll = countAllCollects();
			StatisticsEntry entry = query(query, countAll);
			return new CoverageCheckerImpl(label, entry, countAll);
		}

		@Override
		public CoverageChecker checkPattern(String regex) {
			int countAll = countAllCollects();
			StatisticsEntry entry = matchPattern(regex, countAll);
			return new CoverageCheckerImpl(label, entry, countAll);
		}

	}

	private static class CoverageCheckerImpl implements CoverageChecker {

		private final String label;
		private final StatisticsEntry entry;
		private final int countAll;

		public CoverageCheckerImpl(String label, StatisticsEntry entry, int countAll) {
			this.label = label;
			this.entry = entry;
			this.countAll = countAll;
		}

		@Override
		public void count(Predicate countChecker) {
			if (!countChecker.test(entry.count())) {
				String condition = String.format("Count of %s", entry.count());
				failCondition(condition);
			}
		}

		@Override
		public void count(BiPredicate countChecker) {
			if (!countChecker.test(entry.count(), countAll)) {
				String condition = String.format("Count of (%s, %s)", entry.count(), countAll);
				failCondition(condition);
			}
		}

		@Override
		public void count(Consumer countChecker) {
			count(c -> {
				countChecker.accept(c);
				return true;
			});
		}

		@Override
		public void count(BiConsumer countChecker) {
			count((c, a) -> {
				countChecker.accept(c, a);
				return true;
			});
		}

		@Override
		public void percentage(Predicate percentageChecker) {
			if (!percentageChecker.test(entry.percentage())) {
				String condition = String.format("Percentage of %s", entry.percentage());
				failCondition(condition);
			}
		}

		@Override
		public void percentage(Consumer percentageChecker) {
			percentage(p -> {
				percentageChecker.accept(p);
				return true;
			});
		}

		private void failCondition(String condition) {
			String message = String.format(
				"%s for %s does not fulfill condition%s",
				condition,
				entry.name(),
				statisticsLabel(label)
			);
			fail(message);
		}

		private void fail(String message) {
			throw new AssertionFailedError(message);
		}

	}
}