source: josm/trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerTestIT.java@ 18267

Last change on this file since 18267 was 18267, checked in by Don-vip, 3 years ago

PluginHandlerTestIT: Skip unofficial plugins in headless mode, too much work for us for little added-value

File size: 12.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.plugins;
3
4import static org.junit.jupiter.api.Assertions.assertEquals;
5import static org.junit.jupiter.api.Assertions.assertFalse;
6import static org.junit.jupiter.api.Assertions.assertTrue;
7
8import java.awt.GraphicsEnvironment;
9import java.awt.HeadlessException;
10import java.io.IOException;
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Collection;
14import java.util.HashMap;
15import java.util.Iterator;
16import java.util.List;
17import java.util.Map;
18import java.util.Map.Entry;
19import java.util.Set;
20import java.util.function.Consumer;
21import java.util.stream.Collectors;
22
23import org.junit.jupiter.api.BeforeAll;
24import org.junit.jupiter.api.Test;
25import org.junit.jupiter.api.extension.RegisterExtension;
26import org.openstreetmap.josm.TestUtils;
27import org.openstreetmap.josm.data.Preferences;
28import org.openstreetmap.josm.data.gpx.GpxData;
29import org.openstreetmap.josm.data.osm.DataSet;
30import org.openstreetmap.josm.gui.MainApplication;
31import org.openstreetmap.josm.gui.layer.GpxLayer;
32import org.openstreetmap.josm.gui.layer.Layer;
33import org.openstreetmap.josm.gui.layer.OsmDataLayer;
34import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
35import org.openstreetmap.josm.spi.preferences.Config;
36import org.openstreetmap.josm.testutils.JOSMTestRules;
37import org.openstreetmap.josm.tools.Destroyable;
38import org.openstreetmap.josm.tools.Logging;
39import org.openstreetmap.josm.tools.Utils;
40
41import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
42
43/**
44 * Integration tests of {@link PluginHandler} class.
45 */
46public class PluginHandlerTestIT {
47
48 private static final List<String> errorsToIgnore = new ArrayList<>();
49 /**
50 * Setup test.
51 */
52 @RegisterExtension
53 @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
54 public static JOSMTestRules test = new JOSMTestRules().main().projection().preferences().https()
55 .territories().timeout(10 * 60 * 1000);
56
57 /**
58 * Setup test
59 *
60 * @throws IOException in case of I/O error
61 */
62 @BeforeAll
63 public static void beforeClass() throws IOException {
64 errorsToIgnore.addAll(TestUtils.getIgnoredErrorMessages(PluginHandlerTestIT.class));
65 }
66
67 /**
68 * Test that available plugins rules can be loaded.
69 */
70 @Test
71 void testValidityOfAvailablePlugins() {
72 loadAllPlugins();
73
74 Map<String, Throwable> loadingExceptions = PluginHandler.pluginLoadingExceptions.entrySet().stream()
75 .filter(e -> !(Utils.getRootCause(e.getValue()) instanceof HeadlessException))
76 .collect(Collectors.toMap(e -> e.getKey(), e -> Utils.getRootCause(e.getValue())));
77
78 List<PluginInformation> loadedPlugins = PluginHandler.getPlugins();
79 Map<String, List<String>> invalidManifestEntries = loadedPlugins.stream().filter(pi -> !pi.invalidManifestEntries.isEmpty())
80 .collect(Collectors.toMap(pi -> pi.name, pi -> pi.invalidManifestEntries));
81
82 // Add/remove layers twice to test basic plugin good behaviour
83 Map<String, Throwable> layerExceptions = new HashMap<>();
84 for (int i = 0; i < 2; i++) {
85 OsmDataLayer layer = new OsmDataLayer(new DataSet(), "Layer "+i, null);
86 testPlugin(MainApplication.getLayerManager()::addLayer, layer, layerExceptions, loadedPlugins);
87 testPlugin(MainApplication.getLayerManager()::removeLayer, layer, layerExceptions, loadedPlugins);
88 }
89 for (int i = 0; i < 2; i++) {
90 GpxLayer layer = new GpxLayer(new GpxData(), "Layer "+i);
91 testPlugin(MainApplication.getLayerManager()::addLayer, layer, layerExceptions, loadedPlugins);
92 testPlugin(MainApplication.getLayerManager()::removeLayer, layer, layerExceptions, loadedPlugins);
93 }
94
95 Map<String, Throwable> noRestartExceptions = new HashMap<>();
96 testCompletelyRestartlessPlugins(loadedPlugins, noRestartExceptions);
97
98 debugPrint(invalidManifestEntries);
99 debugPrint(loadingExceptions);
100 debugPrint(layerExceptions);
101 debugPrint(noRestartExceptions);
102
103 invalidManifestEntries = filterKnownErrors(invalidManifestEntries);
104 loadingExceptions = filterKnownErrors(loadingExceptions);
105 layerExceptions = filterKnownErrors(layerExceptions);
106 noRestartExceptions = filterKnownErrors(noRestartExceptions);
107
108 String msg = errMsg("invalidManifestEntries", invalidManifestEntries) + '\n' +
109 errMsg("loadingExceptions", loadingExceptions) + '\n' +
110 errMsg("layerExceptions", layerExceptions) + '\n' +
111 errMsg("noRestartExceptions", noRestartExceptions);
112 assertTrue(invalidManifestEntries.isEmpty()
113 && loadingExceptions.isEmpty()
114 && layerExceptions.isEmpty()
115 && noRestartExceptions.isEmpty(), msg);
116 }
117
118 private static String errMsg(String type, Map<String, ?> map) {
119 return type + ": " + Arrays.toString(map.entrySet().toArray());
120 }
121
122 private static void testCompletelyRestartlessPlugins(List<PluginInformation> loadedPlugins,
123 Map<String, Throwable> noRestartExceptions) {
124 try {
125 List<PluginInformation> restartable = loadedPlugins.parallelStream()
126 .filter(info -> PluginHandler.getPlugin(info.name) instanceof Destroyable)
127 .collect(Collectors.toList());
128 // ensure good plugin behavior with regards to Destroyable (i.e., they can be
129 // removed and readded)
130 for (int i = 0; i < 2; i++) {
131 assertFalse(PluginHandler.removePlugins(restartable), () -> Logging.getLastErrorAndWarnings().toString());
132 List<PluginInformation> notRemovedPlugins = restartable.stream()
133 .filter(info -> PluginHandler.getPlugins().contains(info)).collect(Collectors.toList());
134 assertTrue(notRemovedPlugins.isEmpty(), notRemovedPlugins::toString);
135 loadPlugins(restartable);
136 }
137
138 assertTrue(PluginHandler.removePlugins(loadedPlugins), () -> Logging.getLastErrorAndWarnings().toString());
139 assertTrue(restartable.parallelStream().noneMatch(info -> PluginHandler.getPlugins().contains(info)));
140 } catch (Exception | LinkageError t) {
141 Throwable root = Utils.getRootCause(t);
142 root.printStackTrace();
143 noRestartExceptions.put(findFaultyPlugin(loadedPlugins, root), root);
144 }
145 }
146
147 private static <T> Map<String, T> filterKnownErrors(Map<String, T> errorMap) {
148 return errorMap.entrySet().parallelStream()
149 .filter(entry -> !errorsToIgnore.contains(convertEntryToString(entry)))
150 .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
151 }
152
153 private static void debugPrint(Map<String, ?> invalidManifestEntries) {
154 System.out.println(invalidManifestEntries.entrySet()
155 .stream()
156 .map(e -> convertEntryToString(e))
157 .collect(Collectors.joining(", ")));
158 }
159
160 private static String convertEntryToString(Entry<String, ?> entry) {
161 return entry.getKey() + "=\"" + entry.getValue() + "\"";
162 }
163
164 /**
165 * Downloads and loads all JOSM plugins.
166 */
167 public static void loadAllPlugins() {
168 // Download complete list of plugins
169 ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask(
170 Preferences.main().getOnlinePluginSites());
171 pluginInfoDownloadTask.run();
172 List<PluginInformation> plugins = pluginInfoDownloadTask.getAvailablePlugins();
173 System.out.println("Original plugin list contains " + plugins.size() + " plugins");
174 assertFalse(plugins.isEmpty(), plugins::toString);
175 PluginInformation info = plugins.get(0);
176 assertFalse(info.getName().isEmpty(), info::toString);
177 assertFalse(info.getClass().getName().isEmpty(), info::toString);
178
179 // Filter deprecated and unmaintained ones, or those not responsive enough to match our continuous integration needs
180 List<String> uncooperatingPlugins = Arrays.asList("ebdirigo", "scoutsigns", "josm-config");
181 Set<String> deprecatedPlugins = PluginHandler.getDeprecatedAndUnmaintainedPlugins();
182 for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) {
183 PluginInformation pi = it.next();
184 if (deprecatedPlugins.contains(pi.name) || uncooperatingPlugins.contains(pi.name)) {
185 System.out.println("Ignoring " + pi.name + " (deprecated, unmaintained, or uncooperative)");
186 it.remove();
187 }
188 }
189
190 // On Java < 11 and headless mode, filter plugins requiring JavaFX as Monocle is not available
191 int javaVersion = Utils.getJavaVersion();
192 if (GraphicsEnvironment.isHeadless() && javaVersion < 11) {
193 for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) {
194 PluginInformation pi = it.next();
195 if (pi.getRequiredPlugins().contains("javafx")) {
196 System.out.println("Ignoring " + pi.name + " (requiring JavaFX and we're using Java < 11 in headless mode)");
197 it.remove();
198 }
199 }
200 }
201
202 // Skip unofficial plugins in headless mode, too much work for us for little added-value
203 if (GraphicsEnvironment.isHeadless()) {
204 for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) {
205 PluginInformation pi = it.next();
206 if (pi.isExternal()) {
207 System.out.println("Ignoring " + pi.name + " (unofficial plugin in headless mode)");
208 it.remove();
209 }
210 }
211 }
212
213 System.out.println("Filtered plugin list contains " + plugins.size() + " plugins");
214
215 // Download plugins
216 downloadPlugins(plugins);
217
218 loadPlugins(plugins);
219 }
220
221 static void loadPlugins(List<PluginInformation> plugins) {
222 // Load early plugins
223 PluginHandler.loadEarlyPlugins(null, plugins, null);
224
225 // Load late plugins
226 PluginHandler.loadLatePlugins(null, plugins, null);
227 }
228
229 void testPlugin(Consumer<Layer> consumer, Layer layer,
230 Map<String, Throwable> layerExceptions, Collection<PluginInformation> loadedPlugins) {
231 try {
232 consumer.accept(layer);
233 } catch (Exception | LinkageError t) {
234 Throwable root = Utils.getRootCause(t);
235 root.printStackTrace();
236 layerExceptions.put(findFaultyPlugin(loadedPlugins, root), root);
237 }
238 }
239
240 private static String findFaultyPlugin(Collection<PluginInformation> plugins, Throwable root) {
241 for (PluginInformation p : plugins) {
242 try {
243 ClassLoader cl = PluginHandler.getPluginClassLoader(p.getName());
244 String pluginPackage = cl.loadClass(p.className).getPackage().getName();
245 for (StackTraceElement e : root.getStackTrace()) {
246 try {
247 String stackPackage = cl.loadClass(e.getClassName()).getPackage().getName();
248 if (stackPackage.startsWith(pluginPackage)) {
249 return p.name;
250 }
251 } catch (ClassNotFoundException ex) {
252 System.err.println(ex.getMessage());
253 continue;
254 }
255 }
256 } catch (ClassNotFoundException ex) {
257 System.err.println(ex.getMessage());
258 continue;
259 }
260 }
261 return "<unknown>";
262 }
263
264 /**
265 * Download plugins
266 * @param plugins plugins to download
267 */
268 public static void downloadPlugins(Collection<PluginInformation> plugins) {
269 // Update the locally installed plugins
270 PluginDownloadTask pluginDownloadTask = new PluginDownloadTask(NullProgressMonitor.INSTANCE, plugins, null);
271 // Increase default timeout to avoid random network errors on big jar files
272 int defTimeout = Config.getPref().getInt("socket.timeout.read", 30);
273 Config.getPref().putInt("socket.timeout.read", 2 * defTimeout);
274 pluginDownloadTask.run();
275 // Restore default timeout
276 Config.getPref().putInt("socket.timeout.read", defTimeout);
277 assertTrue(pluginDownloadTask.getFailedPlugins().isEmpty(), pluginDownloadTask.getFailedPlugins()::toString);
278 assertEquals(plugins.size(), pluginDownloadTask.getDownloadedPlugins().size());
279
280 // Update Plugin info for downloaded plugins
281 PluginHandler.refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
282 }
283}
Note: See TracBrowser for help on using the repository browser.