1 /*
2  * Copyright (C) 2018 The Android Open Source Project
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 com.android.server.wm.flicker;
18 
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.Optional;
22 import java.util.concurrent.TimeUnit;
23 import java.util.function.Function;
24 import java.util.stream.Collectors;
25 
26 /**
27  * Collection of functional interfaces and classes representing assertions and their associated
28  * results. Assertions are functions that are applied over a single trace entry and returns a result
29  * which includes a detailed reason if the assertion fails.
30  */
31 public class Assertions {
32     /**
33      * Checks assertion on a single trace entry.
34      *
35      * @param <T> trace entry type to perform the assertion on.
36      */
37     @FunctionalInterface
38     public interface TraceAssertion<T> extends Function<T, Result> {
39         /**
40          * Returns an assertion that represents the logical negation of this assertion.
41          *
42          * @return a assertion that represents the logical negation of this assertion
43          */
negate()44         default TraceAssertion<T> negate() {
45             return (T t) -> apply(t).negate();
46         }
47     }
48 
49     /** Checks assertion on a single layers trace entry. */
50     @FunctionalInterface
51     public interface LayersTraceAssertion extends TraceAssertion<LayersTrace.Entry> {}
52 
53     /**
54      * Utility class to store assertions with an identifier to help generate more useful debug data
55      * when dealing with multiple assertions.
56      */
57     public static class NamedAssertion<T> implements TraceAssertion<T> {
58         private final TraceAssertion<T> assertion;
59         private final String name;
60 
NamedAssertion(TraceAssertion<T> assertion, String name)61         public NamedAssertion(TraceAssertion<T> assertion, String name) {
62             this.assertion = assertion;
63             this.name = name;
64         }
65 
getName()66         public String getName() {
67             return this.name;
68         }
69 
70         @Override
apply(T t)71         public Result apply(T t) {
72             return this.assertion.apply(t);
73         }
74 
75         @Override
toString()76         public String toString() {
77             return "Assertion(" + this.name + ")";
78         }
79     }
80 
81     public static class CompoundAssertion<T> extends NamedAssertion<T> {
82         private final List<NamedAssertion<T>> assertions = new ArrayList<>();
83 
CompoundAssertion(TraceAssertion<T> assertion, String name)84         public CompoundAssertion(TraceAssertion<T> assertion, String name) {
85             super(assertion, name);
86 
87             add(assertion, name);
88         }
89 
add(TraceAssertion<T> assertion, String name)90         public void add(TraceAssertion<T> assertion, String name) {
91             this.assertions.add(new NamedAssertion<>(assertion, name));
92         }
93 
94         @Override
getName()95         public String getName() {
96             return this.assertions.stream().map(p -> p.name).collect(Collectors.joining(" and "));
97         }
98 
99         @Override
apply(T t)100         public Result apply(T t) {
101             List<Result> assertionResults =
102                     this.assertions.stream().map(p -> p.apply(t)).collect(Collectors.toList());
103 
104             boolean passed = assertionResults.stream().allMatch(Result::passed);
105             String reason =
106                     assertionResults
107                             .stream()
108                             .filter(p -> !p.passed())
109                             .map(p -> p.reason)
110                             .collect(Collectors.joining(" and "));
111 
112             Optional<Long> timestamp = assertionResults.stream().map(p -> p.timestamp).findFirst();
113 
114             return timestamp
115                     .map(p -> new Result(passed, p, this.getName(), reason))
116                     .orElseGet(() -> new Result(passed, reason));
117         }
118 
119         @Override
toString()120         public String toString() {
121             return "CompoundAssertion(" + this.getName() + ")";
122         }
123     }
124 
125     /** Contains the result of an assertion including the reason for failed assertions. */
126     public static class Result {
127         public static final String NEGATION_PREFIX = "!";
128         public final boolean success;
129         public final long timestamp;
130         public final String assertionName;
131         public final String reason;
132 
Result(boolean success, long timestamp, String assertionName, String reason)133         public Result(boolean success, long timestamp, String assertionName, String reason) {
134             this.success = success;
135             this.timestamp = timestamp;
136             this.assertionName = assertionName;
137             this.reason = reason;
138         }
139 
Result(boolean success, String reason)140         public Result(boolean success, String reason) {
141             this.success = success;
142             this.reason = reason;
143             this.assertionName = "";
144             this.timestamp = 0;
145         }
146 
147         /** Returns the negated {@code Result} and adds a negation prefix to the assertion name. */
negate()148         public Result negate() {
149             String negatedAssertionName;
150             if (this.assertionName.startsWith(NEGATION_PREFIX)) {
151                 negatedAssertionName = this.assertionName.substring(NEGATION_PREFIX.length() + 1);
152             } else {
153                 negatedAssertionName = NEGATION_PREFIX + this.assertionName;
154             }
155             return new Result(!this.success, this.timestamp, negatedAssertionName, this.reason);
156         }
157 
passed()158         public boolean passed() {
159             return this.success;
160         }
161 
failed()162         public boolean failed() {
163             return !this.success;
164         }
165 
166         @Override
toString()167         public String toString() {
168             return "Timestamp: "
169                     + prettyTimestamp(timestamp)
170                     + "\nAssertion: "
171                     + assertionName
172                     + "\nReason:   "
173                     + reason;
174         }
175 
prettyTimestamp(long timestamp_ns)176         private String prettyTimestamp(long timestamp_ns) {
177             StringBuilder prettyTimestamp = new StringBuilder();
178             TimeUnit[] timeUnits = {
179                 TimeUnit.DAYS,
180                 TimeUnit.HOURS,
181                 TimeUnit.MINUTES,
182                 TimeUnit.SECONDS,
183                 TimeUnit.MILLISECONDS
184             };
185             String[] unitSuffixes = {"d", "h", "m", "s", "ms"};
186 
187             for (int i = 0; i < timeUnits.length; i++) {
188                 long convertedTime = timeUnits[i].convert(timestamp_ns, TimeUnit.NANOSECONDS);
189                 timestamp_ns -= TimeUnit.NANOSECONDS.convert(convertedTime, timeUnits[i]);
190                 prettyTimestamp.append(convertedTime).append(unitSuffixes[i]);
191             }
192 
193             return prettyTimestamp.toString();
194         }
195     }
196 }
197