View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   */
19  package org.apache.rat;
20  
21  import java.io.FileOutputStream;
22  import java.lang.annotation.ElementType;
23  import java.lang.annotation.Retention;
24  import java.lang.annotation.RetentionPolicy;
25  import java.lang.annotation.Target;
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Method;
28  import java.nio.charset.StandardCharsets;
29  import java.nio.file.Path;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.TreeMap;
33  import org.apache.commons.cli.Option;
34  import org.apache.commons.cli.ParseException;
35  import org.apache.commons.lang3.tuple.Pair;
36  import org.apache.rat.commandline.ArgumentContext;
37  import org.apache.rat.document.DocumentName;
38  import org.apache.rat.license.LicenseSetFactory;
39  import org.apache.rat.report.Reportable;
40  import org.apache.rat.test.AbstractConfigurationOptionsProvider;
41  import org.apache.rat.test.utils.OptionFormatter;
42  import org.apache.rat.testhelpers.TestingLog;
43  import org.apache.rat.utils.CasedString;
44  import org.apache.rat.utils.DefaultLog;
45  import org.apache.rat.utils.FileUtils;
46  import org.apache.rat.utils.Log;
47  import org.apache.rat.walker.ArchiveWalker;
48  import org.apache.rat.walker.DirectoryWalker;
49  import org.junit.jupiter.api.AfterAll;
50  import org.junit.jupiter.api.Test;
51  import org.junit.jupiter.api.io.CleanupMode;
52  import org.junit.jupiter.api.io.TempDir;
53  import org.junit.jupiter.params.ParameterizedTest;
54  import org.junit.jupiter.params.provider.ArgumentsProvider;
55  import org.junit.jupiter.params.provider.ArgumentsSource;
56  import org.junit.jupiter.params.provider.ValueSource;
57  
58  import java.io.File;
59  import java.io.IOException;
60  import java.util.ArrayList;
61  import java.util.Collections;
62  import java.util.List;
63  import java.util.concurrent.atomic.AtomicBoolean;
64  
65  import static java.lang.String.format;
66  
67  import static org.assertj.core.api.Assertions.assertThat;
68  import static org.assertj.core.api.Fail.fail;
69  import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
70  
71  public class OptionCollectionTest {
72      @TempDir(cleanup = CleanupMode.NEVER)
73      static Path testPath;
74  
75      @AfterAll
76      static void preserveData() {
77          AbstractConfigurationOptionsProvider.preserveData(testPath.toFile(), "optionTest");
78      }
79  
80      /**
81       * Defines the test method that is stored in a map.
82       */
83      @FunctionalInterface
84      public interface OptionTest {
85          /**
86           * Executes the test and uses fail or asserts to generate failures.
87           */
88          void exec();
89  
90          /**
91           * Execute the test and ensure any failures have the test name added.
92           */
93          default void test() {
94              try {
95                  exec();
96              } catch (AssertionError e) {
97                  throw new AssertionError(formatMsg(e.getMessage()), e);
98              }
99          }
100 
101         /**
102          * Formats the messages by adding the test name.
103          * @param msg the message to reformat.
104          * @return the formatted message.
105          */
106         default String formatMsg(String msg) {
107             return String.format("%s: %s", this, msg);
108         }
109 
110         /**
111          * Get the name of the test.
112          * By default, this method returns {@code toString()}.
113          * @return the name of the test.
114          */
115         default String name() {
116             return toString();
117         }
118 
119         /**
120          * Creates a named OptionTest.
121          * @param name the name of the test.
122          * @param test the test to execute.
123          * @return a named option test.
124          */
125         static OptionTest namedTest(String providerName, String name, OptionTest test) {
126             return new OptionTest() {
127                 @Override
128                 public void exec() {
129                     test.exec();
130                 }
131                 @Override
132                 public String toString() {
133                     return String.join(":", providerName, name);
134                 }
135                 @Override
136                 public String name() {
137                     return name;
138                 }
139             };
140         }
141     }
142 
143     /**
144      * A test function. Used to annotate methods in test providers.
145      */
146     @Retention(RetentionPolicy.RUNTIME)
147     @Target(ElementType.METHOD)
148     public @interface TestFunction {
149     }
150 
151     /**
152      * Gets the test path for.
153      * @return the test path.
154      */
155     public static Path getTestPath() {
156         return testPath;
157     }
158 
159     /**
160      * Process methods in a test provider.
161      * Tests are detected by looking for the {@link TestFunction} annotation.
162      * @param testProvider the test provider.
163      * @return a map of named tests to a named OptionTest.
164      */
165     public static Map<String, OptionTest> processTestFunctionAnnotations(Object testProvider) {
166         final int testLength = 4;
167         final Class<?> clazz = testProvider.getClass();
168         final Map<String, OptionTest> result = new TreeMap<>();
169         for (Method method : clazz.getDeclaredMethods()) {
170             if (method.isAnnotationPresent(TestFunction.class) && method.getParameterCount() == 0 && method.getReturnType() == void.class) {
171                 String name = method.getName();
172                 if (name.endsWith("Test")) {
173                     name = name.substring(0, name.length() - testLength);
174                 }
175                 if (name.startsWith("test")) {
176                     name = name.substring(testLength);
177                 }
178                 name = new CasedString(CasedString.StringCase.CAMEL, name).toCase(CasedString.StringCase.KEBAB).toLowerCase(Locale.ROOT);
179                 result.put(name, OptionTest.namedTest(clazz.getName(), name, () -> {
180                             try {
181                                 method.invoke(testProvider);
182                             } catch (IllegalAccessException | InvocationTargetException e) {
183                                 throw new AssertionError(e);
184                             }
185                         }
186                 ));
187             }
188         }
189         return result;
190     }
191 
192     @Test
193     public void testDeprecatedUseLogged() throws IOException {
194         TestingLog log = new TestingLog();
195         try {
196             FileUtils.mkDir(testPath.resolve("target").toFile());
197             DefaultLog.setInstance(log);
198             String[] args = {"--dir", "target", "-a"};
199             ReportConfiguration config = OptionCollection.parseCommands(testPath.toFile(), args, o -> fail("Help printed"), true);
200             assertThat(config).isNotNull();
201         } finally {
202             DefaultLog.setInstance(null);
203         }
204         assertThat(log.getCaptured()).containsOnlyOnce("WARN: Option [-d, --dir] used. Deprecated for removal since 0.17: Use the standard '--'")
205                         .containsOnlyOnce("WARN: Option [-a] used. Deprecated for removal since 0.17: Use --edit-license");
206     }
207 
208     @Test
209     public void testDirOptionCapturesDirectoryToScan() throws IOException {
210         TestingLog log = new TestingLog();
211         ReportConfiguration config;
212         try {
213             DefaultLog.setInstance(log);
214             String[] args = {"--dir", testPath.toFile().getAbsolutePath()};
215             config = OptionCollection.parseCommands(testPath.toFile(), args, (o) -> {
216             }, true);
217         } finally {
218             DefaultLog.setInstance(null);
219         }
220         assertThat(config).isNotNull();
221         assertThat(log.getCaptured()).containsOnlyOnce("WARN: Option [-d, --dir] used. Deprecated for removal since 0.17: Use the standard '--'");
222     }
223 
224     @Test
225     public void testShortenedOptions() throws IOException {
226         String[] args = {"--output-lic", "ALL"};
227         ReportConfiguration config = OptionCollection.parseCommands(testPath.toFile(), args, (o) -> {
228         }, true);
229         assertThat(config).isNotNull();
230         assertThat(config.listLicenses()).isEqualTo(LicenseSetFactory.LicenseFilter.ALL);
231     }
232 
233     @Test
234     public void testDefaultConfiguration() throws ParseException {
235         String[] empty = {};
236         ArgumentContext context = new ArgumentContext(new File("."), OptionCollection.buildOptions(), empty);
237         ReportConfiguration config = OptionCollection.createConfiguration(context);
238         ReportConfigurationTest.validateDefault(config);
239     }
240 
241     @ParameterizedTest
242     @ValueSource(strings = { ".", "./", "target", "./target" })
243     public void getReportableTest(String fName) throws IOException {
244         File base = new File(fName);
245         DocumentName.FSInfo fsInfo = new DocumentName.FSInfo(testPath.getFileSystem());
246         String expected = fsInfo.normalize(base.getAbsolutePath());
247         ReportConfiguration config = OptionCollection.parseCommands(testPath.toFile(), new String[]{fName}, o -> fail("Help called"), false);
248         Reportable reportable = OptionCollection.getReportable(base, config);
249         assertThat(reportable).as(() -> format("'%s' returned null", fName)).isNotNull();
250         assertThat(reportable.name().getName()).isEqualTo(expected);
251     }
252 
253     @Test
254     void getReportable() throws IOException {
255         File dir1 = testPath.resolve("dir1").toFile();
256         assertThat(OptionCollection.getReportable(dir1, new ReportConfiguration())).isNull();
257 
258         assertThat(dir1.mkdir()).isTrue();
259         ReportConfiguration reportConfiguration = new ReportConfiguration();
260 
261         File dir2 = testPath.resolve("dir2").toFile();
262         reportConfiguration.addExcludedPatterns(List.of(dir2.getName()));
263         assertThat(OptionCollection.getReportable(dir2, reportConfiguration)).isNull();
264 
265         Reportable reportable = OptionCollection.getReportable(dir1, new ReportConfiguration());
266         assertThat(reportable).isInstanceOf(DirectoryWalker.class);
267 
268         File file1 = new File(dir1, "file1");
269         try (FileOutputStream fos = new FileOutputStream(file1)) {
270             fos.write("Hello world".getBytes(StandardCharsets.UTF_8));
271         }
272         reportable = OptionCollection.getReportable(file1, new ReportConfiguration());
273         assertThat(reportable).isInstanceOf(ArchiveWalker.class);
274     }
275 
276     /**
277      * A parameterized test for the options.
278      * @param name the name of the test.
279      * @param test the option test to execute.
280      */
281     @ParameterizedTest( name = "{index} {0}")
282     @ArgumentsSource(ArgOptionsProvider.class)
283     public void testOptionsUpdateConfig(String name, OptionTest test) {
284         DefaultLog.getInstance().log(Log.Level.INFO, "Running test for: " + name);
285         test.test();
286     }
287 
288     /**
289      * A class to provide the Options and tests to the testOptionsUpdateConfig.
290      */
291     static class ArgOptionsProvider extends AbstractConfigurationOptionsProvider implements ArgumentsProvider {
292 
293         /** A flag to determine if help was called. */
294         final AtomicBoolean helpCalled = new AtomicBoolean(false);
295 
296         /**
297          * Provide a test for the help option.
298          */
299         public void helpTest() {
300             String[] args = { OptionFormatter.longOpt(OptionCollection.HELP) };
301 
302             assertDoesNotThrow(() -> {
303                 ReportConfiguration config = OptionCollection.parseCommands(testPath.toFile(), args, o -> helpCalled.set(true), true);
304                 assertThat(config).as("Should not have config").isNull();
305                 assertThat(helpCalled.get()).as("Help was not called").isTrue();
306             });
307         }
308 
309         /**
310          * Constructor. Sets the baseDir and loads the testMap.
311          */
312         public ArgOptionsProvider() {
313             super("ArgsOptionsProvider", Collections.emptyList(), testPath.toFile());
314             addTest(OptionCollectionTest.OptionTest.namedTest(providerName, "help", this::helpTest));
315         }
316 
317         /**
318          * Generate a ReportConfiguration from a set of arguments.
319          * Forces the {@code helpCalled} flag to be reset.
320          * @param args the arguments.
321          * @return A ReportConfiguration
322          * @throws IOException on critical error.
323          */
324         protected final ReportConfiguration generateConfig(List<Pair<Option, String[]>> args) throws IOException {
325             helpCalled.set(false);
326             List<String> sArgs = new ArrayList<>();
327             for (Pair<Option, String[]> pair : args) {
328                 if (pair.getKey() != null) {
329                     sArgs.add("--" + pair.getKey().getLongOpt());
330                     String[] oArgs = pair.getValue();
331                     if (oArgs != null) {
332                         Collections.addAll(sArgs, oArgs);
333                     }
334                 }
335             }
336             ReportConfiguration config = OptionCollection.parseCommands(testPath.toFile(), sArgs.toArray(new String[0]), o -> helpCalled.set(true), true);
337             assertThat(helpCalled.get()).as("Help was called").isFalse();
338             return config;
339         }
340     }
341 }