1 /* 2 * Copyright (C) 2022 The Dagger Authors. 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 dagger.testing.golden; 18 19 import androidx.room.compiler.processing.util.Source; 20 import com.google.common.io.Resources; 21 import com.google.testing.compile.JavaFileObjects; 22 import java.io.IOException; 23 import java.net.URL; 24 import java.nio.charset.StandardCharsets; 25 import java.util.regex.Matcher; 26 import java.util.regex.Pattern; 27 import javax.tools.JavaFileObject; 28 import org.junit.rules.TestRule; 29 import org.junit.runner.Description; 30 import org.junit.runners.model.Statement; 31 32 33 /** A test rule that manages golden files for tests. */ 34 public final class GoldenFileRule implements TestRule { 35 /** The generated import used in the golden files */ 36 private static final String GOLDEN_GENERATED_IMPORT = 37 "import javax.annotation.processing.Generated;"; 38 39 /** The generated import used with the current jdk version */ 40 private static final String JDK_GENERATED_IMPORT = 41 isBeforeJava9() 42 ? "import javax.annotation.Generated;" 43 : "import javax.annotation.processing.Generated;"; 44 isBeforeJava9()45 private static boolean isBeforeJava9() { 46 try { 47 Class.forName("java.lang.Module"); 48 return false; 49 } catch (ClassNotFoundException e) { 50 return true; 51 } 52 } 53 54 // Parameterized arguments in junit4 are added in brackets to the end of test methods, e.g. 55 // `myTestMethod[testParam1=FOO,testParam2=BAR]`. This pattern captures theses into two separate 56 // groups, `<GROUP1>[<GROUP2>]` to make it easier when generating the golden file name. 57 private static final Pattern JUNIT_PARAMETERIZED_METHOD = Pattern.compile("(.*?)\\[(.*?)\\]"); 58 59 private Description description; 60 61 @Override apply(Statement base, Description description)62 public Statement apply(Statement base, Description description) { 63 this.description = description; 64 return base; 65 } 66 67 /** 68 * Returns the golden file as a {@link Source} containing the file's content. 69 * 70 * <p>If the golden file does not exist, the returned file object contains an error message 71 * pointing to the location of the missing golden file. This can be used with scripting tools to 72 * output the correct golden file in the proper location. 73 */ goldenSource(String generatedFilePath)74 public Source goldenSource(String generatedFilePath) { 75 // Note: we wrap the IOException in a RuntimeException so that this can be called from within 76 // the lambda required by XProcessing's testing APIs. We could avoid this by calling this method 77 // outside of the lambda, but that seems like an non-worthwile hit to readability. 78 try { 79 return Source.Companion.java( 80 generatedFilePath, goldenFileContent(generatedFilePath.replace('/', '.'))); 81 } catch (IOException e) { 82 throw new RuntimeException(e); 83 } 84 } 85 86 /** 87 * Returns the golden file as a {@link JavaFileObject} containing the file's content. 88 * 89 * If the golden file does not exist, the returned file object contain an error message pointing 90 * to the location of the missing golden file. This can be used with scripting tools to output 91 * the correct golden file in the proper location. 92 */ goldenFile(String qualifiedName)93 public JavaFileObject goldenFile(String qualifiedName) throws IOException { 94 return JavaFileObjects.forSourceLines(qualifiedName, goldenFileContent(qualifiedName)); 95 } 96 97 /** 98 * Returns the golden file content. 99 * 100 * If the golden file does not exist, the returned content contains an error message pointing 101 * to the location of the missing golden file. This can be used with scripting tools to output 102 * the correct golden file in the proper location. 103 */ goldenFileContent(String qualifiedName)104 public String goldenFileContent(String qualifiedName) throws IOException { 105 String fileName = 106 String.format( 107 "%s_%s_%s", 108 description.getTestClass().getSimpleName(), 109 getFormattedMethodName(description), 110 qualifiedName); 111 112 URL url = description.getTestClass().getResource("goldens/" + fileName); 113 return url == null 114 // If the golden file does not exist, create a fake file with a comment pointing to the 115 // missing golden file. This is helpful for scripts that need to generate golden files from 116 // the test failures. 117 ? "// Error: Missing golden file for goldens/" + fileName 118 // The goldens are generated using jdk 11, so we use this replacement to allow the 119 // goldens to also work when compiling using jdk < 9. 120 : Resources.toString(url, StandardCharsets.UTF_8) 121 .replace(GOLDEN_GENERATED_IMPORT, JDK_GENERATED_IMPORT); 122 } 123 124 /** 125 * Returns the formatted method name for the given description. 126 * 127 * <p>If this is not a parameterized test, we return the method name as is. If it is a 128 * parameterized test, we format it from {@code someTestMethod[PARAMETER]} to 129 * {@code someTestMethod_PARAMETER} to avoid brackets in the name. 130 */ getFormattedMethodName(Description description)131 private static String getFormattedMethodName(Description description) { 132 Matcher matcher = JUNIT_PARAMETERIZED_METHOD.matcher(description.getMethodName()); 133 134 // If this is a parameterized method, separate the parameters with an underscore 135 return matcher.find() ? matcher.group(1) + "_" + matcher.group(2) : description.getMethodName(); 136 } 137 } 138