1 /*
2  * Copyright 2021 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 androidx.benchmark
18 
19 import androidx.annotation.RestrictTo
20 
21 /** Represents an error in configuration of a benchmark. */
22 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
23 data class ConfigurationError(
24     /**
25      * All-caps, publicly visible ID for the error.
26      *
27      * Used for suppression via instrumentation arguments.
28      */
29     val id: String,
30 
31     /** One-line summary of the problem. */
32     val summary: String,
33 
34     /** Multi-line, preformatted detailed description of the problem. */
35     val message: String
36 ) {
37     init {
38         validateParams(id, summary)
39     }
40 
41     companion object {
validateParamsnull42         internal fun validateParams(id: String, summary: String) {
43             require(!id.contains("[a-z]".toRegex())) {
44                 "IDs must be ALL-CAPs by convention (id=$id)"
45             }
46             require(!id.contains("_")) {
47                 "Use hyphen instead of underscore for consistency (id=$id)"
48             }
49             require(!summary.contains("\n")) { "Summary must be one line" }
50         }
51     }
52 
53     /** Representation of suppressed errors during a running benchmark. */
54     class SuppressionState(
55         /** Prefix for output data to mark as potentially invalid. */
56         val prefix: String,
57 
58         /** Warning message to present to the user. */
59         val warningMessage: String
60     )
61 }
62 
63 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
conditionalErrornull64 fun conditionalError(
65     hasError: Boolean,
66     id: String,
67     summary: String,
68     message: String
69 ): ConfigurationError? {
70     // validation done here *and* in constructor to ensure it happens even when error doesn't fire
71     ConfigurationError.validateParams(id, summary)
72     return if (hasError) {
73         ConfigurationError(id, summary, message)
74     } else null
75 }
76 
prettyPrintnull77 internal fun List<ConfigurationError>.prettyPrint(prefix: String): String {
78     return joinToString("\n") { prefix + it.summary + "\n" + it.message.prependIndent() + "\n" }
79 }
80 
81 /**
82  * Throw an AssertionError if the list contains an unsuppressed error, and return either a
83  * SuppressionState if errors are suppressed, or null otherwise.
84  */
85 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Listnull86 fun List<ConfigurationError>.checkAndGetSuppressionState(
87     suppressedErrorIds: Set<String>,
88 ): ConfigurationError.SuppressionState? {
89     if (isEmpty()) {
90         return null
91     }
92 
93     val (suppressed, unsuppressed) = partition { suppressedErrorIds.contains(it.id) }
94 
95     val prefix = this.joinToString("_") { it.id } + "_"
96 
97     // either fail and report all unsuppressed errors ...
98     if (unsuppressed.isNotEmpty() && !Arguments.dryRunMode) {
99         val unsuppressedString = unsuppressed.joinToString(" ") { it.id }
100         val suppressedString = suppressed.joinToString(" ") { it.id }
101         val howToSuppressString = this.joinToString(",") { it.id }
102         throw AssertionError(
103             """
104                 |ERRORS (not suppressed): $unsuppressedString
105                 |WARNINGS (suppressed): $suppressedString
106                 |
107                 |${unsuppressed.prettyPrint("ERROR: ")}
108                 |While you can suppress these errors (turning them into warnings)
109                 |PLEASE NOTE THAT EACH SUPPRESSED ERROR COMPROMISES ACCURACY
110                 |
111                 |// Sample suppression, in a benchmark module's build.gradle:
112                 |android {
113                 |    defaultConfig {
114                 |        testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "$howToSuppressString"
115                 |    }
116                 |}
117             """
118                 .trimMargin()
119         )
120     }
121 
122     // ... or report all errors as suppressed
123     return ConfigurationError.SuppressionState(prefix, prettyPrint("WARNING: "))
124 }
125