View Javadoc
1   /*-
2    * #%L
3    * io.earcam.instrumental.module.jpms
4    * %%
5    * Copyright (C) 2018 earcam
6    * %%
7    * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
8    * 
9    * You <b>must</b> choose to accept, in full - any individual or combination of 
10   * the following licenses:
11   * <ul>
12   * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
13   * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
14   * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
15   * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
16   * </ul>
17   * #L%
18   */
19  package io.earcam.instrumental.module.jpms;
20  
21  import static io.earcam.instrumental.module.jpms.Access.ACC_MANDATED;
22  import static io.earcam.instrumental.module.jpms.Access.ACC_STATIC_PHASE;
23  import static io.earcam.instrumental.module.jpms.Access.ACC_SYNTHETIC;
24  import static io.earcam.instrumental.module.jpms.Access.ACC_TRANSITIVE;
25  import static io.earcam.instrumental.module.jpms.ModuleInfo.moduleInfo;
26  import static io.earcam.instrumental.module.jpms.ModuleModifier.OPEN;
27  import static io.earcam.instrumental.module.jpms.ModuleModifier.SYNTHETIC;
28  import static io.earcam.instrumental.module.jpms.RequireModifier.STATIC;
29  import static io.earcam.instrumental.module.jpms.RequireModifier.TRANSITIVE;
30  import static java.nio.charset.StandardCharsets.UTF_8;
31  import static java.time.ZoneId.systemDefault;
32  import static java.util.Arrays.asList;
33  import static java.util.EnumSet.of;
34  import static java.util.stream.Collectors.toList;
35  import static org.hamcrest.MatcherAssert.assertThat;
36  import static org.hamcrest.Matchers.aMapWithSize;
37  import static org.hamcrest.Matchers.allOf;
38  import static org.hamcrest.Matchers.anEmptyMap;
39  import static org.hamcrest.Matchers.arrayContaining;
40  import static org.hamcrest.Matchers.*;
41  import static org.hamcrest.Matchers.containsInAnyOrder;
42  import static org.hamcrest.Matchers.containsString;
43  import static org.hamcrest.Matchers.equalTo;
44  import static org.hamcrest.Matchers.equalToIgnoringWhiteSpace;
45  import static org.hamcrest.Matchers.hasEntry;
46  import static org.hamcrest.Matchers.hasToString;
47  import static org.hamcrest.Matchers.is;
48  import static org.hamcrest.Matchers.not;
49  import static org.junit.jupiter.api.Assertions.assertFalse;
50  import static org.junit.jupiter.api.Assertions.fail;
51  import static org.objectweb.asm.Opcodes.ASM6;
52  
53  import java.io.ByteArrayInputStream;
54  import java.io.FileInputStream;
55  import java.io.FileOutputStream;
56  import java.io.IOException;
57  import java.io.InputStream;
58  import java.nio.file.Path;
59  import java.nio.file.Paths;
60  import java.time.LocalDateTime;
61  import java.util.ArrayList;
62  import java.util.Arrays;
63  import java.util.Comparator;
64  import java.util.EnumSet;
65  import java.util.List;
66  import java.util.Optional;
67  import java.util.TreeSet;
68  import java.util.UUID;
69  import java.util.jar.JarInputStream;
70  import java.util.stream.Stream;
71  
72  import org.junit.jupiter.api.Test;
73  import org.objectweb.asm.ClassReader;
74  import org.objectweb.asm.ClassVisitor;
75  import org.objectweb.asm.ModuleVisitor;
76  import org.ops4j.pax.tinybundles.core.TinyBundles;
77  
78  import com.acme.meh.DummyComparator;
79  
80  import io.earcam.utilitarian.io.IoStreams;
81  
82  public class DefaultModuleInfoTest {
83  
84  	DefaultModuleInfo module = (DefaultModuleInfo) createWithPrimitives().construct();
85  
86  
87  	private static ModuleInfoBuilder createWithPrimitives()
88  	{
89  		return create()
90  				.exporting("com.acme.exporting", ACC_MANDATED, "mod.a", "mod.b")
91  				.opening("com.acme.opening", ACC_MANDATED, "mod.x", "mod.y")
92  				.requiring("java.base", ACC_MANDATED, "9");
93  
94  	}
95  
96  
97  	private static ModuleInfoBuilder createWithCollections()
98  	{
99  		return create()
100 				.exporting("com.acme.exporting", EnumSet.of(ExportModifier.MANDATED), new TreeSet<>(asList("mod.a", "mod.b")))
101 				.opening("com.acme.opening", EnumSet.of(ExportModifier.MANDATED), new TreeSet<>(asList("mod.x", "mod.y")))
102 				.requiring("java.base", EnumSet.of(RequireModifier.MANDATED), "9");
103 	}
104 
105 
106 	private static ModuleInfoBuilder create()
107 	{
108 		return ModuleInfo.moduleInfo()
109 				.named("com.acme.module.aye")
110 				.versioned("42.0.0")
111 				.withAccess(ACC_MANDATED)
112 				.providing("com.acme.api.Service", "com.acme.imp.DefaultService")
113 				.using("com.acme.api.AuthenticationService")
114 				.launching("main.Clazz")
115 				.packaging("com.acme.imp.internal");
116 
117 	}
118 
119 
120 	@Test
121 	public void notEqualWhenNameDiffers()
122 	{
123 		ModuleInfo other = createWithCollections().named("a.rose.by.any.other").construct();
124 
125 		assertThat(module, is(not(equalTo(other))));
126 	}
127 
128 
129 	@Test
130 	public void notEqualWhenVersionDiffers()
131 	{
132 		ModuleInfo other = create().versioned("1001.0.999998").construct();
133 
134 		assertThat(module, is(not(equalTo(other))));
135 	}
136 
137 
138 	@Test
139 	public void notEqualWhenAccessDiffers()
140 	{
141 		ModuleInfo other = createWithCollections().withAccess(ACC_SYNTHETIC).construct();
142 
143 		assertThat(module, is(not(equalTo(other))));
144 	}
145 
146 
147 	@Test
148 	public void notEqualWhenExportsDiffer()
149 	{
150 		ModuleInfo other = createWithCollections().exporting("some.other", ACC_MANDATED, "mod.z").construct();
151 
152 		assertThat(module, is(not(equalTo(other))));
153 	}
154 
155 
156 	@Test
157 	public void notEqualWhenOpensDiffer()
158 	{
159 		ModuleInfo other = createWithPrimitives().opening("some.other", ACC_MANDATED, "mod.z").construct();
160 
161 		assertThat(module, is(not(equalTo(other))));
162 	}
163 
164 
165 	@Test
166 	public void notEqualWhenProvideImplementationsDiffer()
167 	{
168 		ModuleInfo other = createWithCollections().providing("com.acme.api.Service", "com.acme.other.imp.NotTheUsualService").construct();
169 
170 		assertThat(module, is(not(equalTo(other))));
171 	}
172 
173 
174 	@Test
175 	public void notEqualWhenProvidesDiffer()
176 	{
177 		ModuleInfo other = createWithPrimitives().providing("some.other.api.Funky", "some.other.imp.FunkyChicken").construct();
178 
179 		assertThat(module, is(not(equalTo(other))));
180 	}
181 
182 
183 	@Test
184 	public void notEqualWhenRequiresDiffer()
185 	{
186 		ModuleInfo other = createWithCollections().requiring("module.x", ACC_MANDATED, "1001").construct();
187 
188 		assertThat(module, is(not(equalTo(other))));
189 	}
190 
191 
192 	@Test
193 	public void notEqualWhenUseDiffers()
194 	{
195 		ModuleInfo other = createWithPrimitives().using("some.other.api.Funky").construct();
196 
197 		assertThat(module, is(not(equalTo(other))));
198 	}
199 
200 
201 	@Test
202 	public void notEqualWhenUsesDiffer()
203 	{
204 		TreeSet<String> uses = new TreeSet<>();
205 		uses.add("some.other.api.Funky");
206 		uses.add("some.other.api.Chicken");
207 
208 		ModuleInfo other = createWithPrimitives().using(uses).construct();
209 
210 		assertThat(module, is(not(equalTo(other))));
211 	}
212 
213 
214 	@Test
215 	public void notEqualWhenMainClassDiffers()
216 	{
217 		ModuleInfo other = createWithCollections().launching("some.other.imp.Main").construct();
218 
219 		assertThat(module, is(not(equalTo(other))));
220 	}
221 
222 
223 	@Test
224 	public void notEqualWhenPackageDiffers()
225 	{
226 		ModuleInfo other = createWithPrimitives().packaging("some.other").construct();
227 
228 		assertThat(module, is(not(equalTo(other))));
229 	}
230 
231 
232 	@Test
233 	public void notEqualWhenPackagesDiffer()
234 	{
235 		TreeSet<CharSequence> packages = new TreeSet<>();
236 		packages.add("some.other");
237 		packages.add("and.another");
238 		ModuleInfo other = createWithPrimitives().packaging(packages).construct();
239 
240 		assertThat(module, is(not(equalTo(other))));
241 	}
242 
243 
244 	@Test
245 	public void notEqualToNullType()
246 	{
247 		assertFalse(module.equals((ModuleInfo) null));
248 	}
249 
250 
251 	@Test
252 	public void notEqualToNullObject()
253 	{
254 		assertFalse(module.equals((Object) null));
255 	}
256 
257 
258 	@Test
259 	public void hashCodeShouldBeNonZero()
260 	{
261 		assertThat(module.hashCode(), is(not(0)));
262 	}
263 
264 
265 	@Test
266 	public void equal()
267 	{
268 		ModuleInfo identical = createWithCollections().construct();
269 		assertThat(module, is(equalTo(identical)));
270 		assertThat(module.hashCode(), is(equalTo(identical.hashCode())));
271 	}
272 
273 
274 	@Test
275 	public void symmetricBytecode()
276 	{
277 		byte[] bytecode = module.toBytecode();
278 
279 		ModuleInfo rehydrated = ModuleInfo.read(bytecode);
280 
281 		assertThat(module, is(equalTo(rehydrated)));
282 	}
283 
284 
285 	@Test
286 	public void deconstructReturnSameInstance()
287 	{
288 		ModuleInfoBuilder deconstructed = module.deconstruct();
289 
290 		assertThat(deconstructed, is(sameInstance(module)));
291 	}
292 
293 
294 	@Test
295 	public void bytecodeOnlyContainsInternalNames()
296 	{
297 		byte[] bytecode = module.toBytecode();
298 
299 		List<String> qualifiedNames = new ArrayList<>();
300 
301 		ClassVisitor classVisitor = new ClassVisitor(ASM6) {
302 			@Override
303 			public ModuleVisitor visitModule(String name, int access, String version)
304 			{
305 				return new ModuleVisitor(ASM6, super.visitModule(name, access, version)) {
306 
307 					@Override
308 					public void visitExport(String packaze, int access, String... modules)
309 					{
310 						qualifiedNames.add(packaze);
311 						super.visitExport(packaze, access, modules);
312 					}
313 
314 
315 					@Override
316 					public void visitMainClass(String mainClass)
317 					{
318 						qualifiedNames.add(mainClass);
319 						super.visitMainClass(mainClass);
320 					}
321 
322 
323 					@Override
324 					public void visitOpen(String packaze, int access, String... modules)
325 					{
326 						qualifiedNames.add(packaze);
327 						super.visitOpen(packaze, access, modules);
328 					}
329 
330 
331 					@Override
332 					public void visitPackage(String packaze)
333 					{
334 						qualifiedNames.add(packaze);
335 						super.visitPackage(packaze);
336 					}
337 
338 
339 					@Override
340 					public void visitProvide(String service, String... providers)
341 					{
342 						qualifiedNames.add(service);
343 						Arrays.stream(providers).forEach(qualifiedNames::add);
344 						super.visitProvide(service, providers);
345 					}
346 
347 
348 					@Override
349 					public void visitUse(String service)
350 					{
351 						qualifiedNames.add(service);
352 						super.visitUse(service);
353 					}
354 				};
355 			}
356 		};
357 
358 		ClassReader reader = new ClassReader(bytecode);
359 		reader.accept(classVisitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
360 
361 		Stream<String> stream = Stream.of(BytecodeWriter.internalName(module.mainClass()));
362 
363 		stream = Stream.concat(
364 				stream,
365 				module.packages().stream().map(BytecodeWriter::internalName));
366 
367 		stream = Stream.concat(
368 				stream,
369 				module.exports().stream().map(Export::paquet).map(BytecodeWriter::internalName));
370 
371 		stream = Stream.concat(
372 				stream,
373 				module.opens().stream().map(Export::paquet).map(BytecodeWriter::internalName));
374 
375 		stream = Stream.concat(
376 				stream,
377 				module.uses().stream().map(BytecodeWriter::internalName));
378 
379 		stream = Stream.concat(
380 				stream,
381 				module.provides().entrySet().stream()
382 						.flatMap(e -> Stream.concat(Stream.of(e.getKey()), Arrays.stream(e.getValue())))
383 						.map(BytecodeWriter::internalName));
384 
385 		List<String> expected = stream.collect(toList());
386 
387 		// note order dependent..
388 		assertThat(qualifiedNames, equalTo(expected));
389 	}
390 
391 
392 	@Test
393 	public void versionIsOptional()
394 	{
395 		byte[] bytecode = module.versioned(null).construct().toBytecode();
396 
397 		ModuleInfo rehydrated = ModuleInfo.read(bytecode);
398 
399 		assertThat(module, is(equalTo(rehydrated)));
400 	}
401 
402 
403 	@Test
404 	public void versionIsOptionalFromInputStream() throws IOException
405 	{
406 		byte[] bytecode = module.versioned(null).construct().toBytecode();
407 
408 		ModuleInfo rehydrated = ModuleInfo.read(new ByteArrayInputStream(bytecode));
409 
410 		assertThat(module, is(equalTo(rehydrated)));
411 	}
412 
413 
414 	@Test
415 	public void modifierIsMappedFromAccess()
416 	{
417 		ModuleInfo m = createWithCollections().withAccess(ACC_SYNTHETIC).construct();
418 
419 		assertThat(m.modifiers(), containsInAnyOrder(SYNTHETIC));
420 	}
421 
422 
423 	@Test
424 	public void hardedCodedToString()
425 	{
426 		String expected = "/**\n" +
427 				" * @version 42.0.0\n" +
428 				" * @modifiers mandated\n" +
429 				" * @package com.acme.imp.internal\n" +
430 				" */\n" +
431 				"module com.acme.module.aye {\n" +
432 				"	/**\n" +
433 				"	 * @version 9\n" +
434 				"	 * @modifiers mandated\n" +
435 				"	 */\n" +
436 				"	requires java.base;\n" +
437 				"	/**\n" +
438 				"	 * @modifiers mandated\n" +
439 				"	 */\n" +
440 				"	exports com.acme.exporting to \n" +
441 				"		mod.a,\n" +
442 				"		mod.b;\n" +
443 				"	/**\n" +
444 				"	 * @modifiers mandated\n" +
445 				"	 */\n" +
446 				"	opens com.acme.opening to \n" +
447 				"		mod.x,\n" +
448 				"		mod.y;\n" +
449 				"	uses com.acme.api.AuthenticationService;\n" +
450 				"	provides com.acme.api.Service with \n" +
451 				"		com.acme.imp.DefaultService;\n" +
452 				"}\n" +
453 				"";
454 
455 		assertThat(module, hasToString(equalToIgnoringWhiteSpace(expected)));
456 	}
457 
458 
459 	@Test
460 	public void openModuleToString()
461 	{
462 		// EARCAM_SNIPPET_BEGIN: to-string-is-source
463 		ModuleInfo openModule = ModuleInfo.moduleInfo()
464 				.named("com.acme.oh.pan")
465 				.versioned("99")
466 				.withAccess(of(OPEN))
467 				.requiring("java.base", ACC_MANDATED, "9")
468 				.construct();
469 
470 		String expected = "/**\n" +
471 				" * @version 99\n" +
472 				" * @modifiers open\n" +
473 				" */\n" +
474 				"open module com.acme.oh.pan {\n" +
475 				"	/**\n" +
476 				"	 * @version 9\n" +
477 				"	 * @modifiers mandated" +
478 				"	 */\n" +
479 				"	requires java.base;\n" +
480 				"}\n";
481 
482 		assertThat(openModule, hasToString(equalToIgnoringWhiteSpace(expected)));
483 		// EARCAM_SNIPPET_END: to-string-is-source
484 	}
485 
486 
487 	@Test
488 	public void hardedCodedToStringForVirtuallyEmptyModule()
489 	{
490 
491 		ModuleInfo m = ModuleInfo.moduleInfo()
492 				.named("com.acme.empty.virtually")
493 				.requiring("java.base", 0, null)
494 				.exporting("com.acme.much.not", ACC_MANDATED)
495 				.opening("com.acme.innards", 0)
496 				.construct();
497 
498 		String expected = "module com.acme.empty.virtually {\n" +
499 				"	requires java.base;\n" +
500 				"	/**\n" +
501 				"	 * @modifiers mandated\n" +
502 				"	 */\n" +
503 				"	exports com.acme.much.not;\n" +
504 				"	opens com.acme.innards;\n" +
505 				"}\n" +
506 				"";
507 
508 		assertThat(m, hasToString(equalToIgnoringWhiteSpace(expected)));
509 	}
510 
511 
512 	@Test
513 	public void requiresFromOtherModule()
514 	{
515 		ModuleInfo required = moduleInfo()
516 				.named("am.required")
517 				.versioned("1.2.3")
518 				.withAccess(ACC_MANDATED)
519 				.construct();
520 
521 		ModuleInfo requiring = ModuleInfo.moduleInfo()
522 				.named("am.requiring")
523 				.requiring(required)
524 				.construct();
525 
526 		assertThat(requiring.requires(), contains(new Require(required.name(), required.access(), required.version())));
527 	}
528 
529 
530 	@Test
531 	public void requiresWithBothTransitiveAndStaticIs()
532 	{
533 		final String badRequireModule = "not.like.this";
534 		try {
535 			ModuleInfo.moduleInfo()
536 					.named("com.acme.invalid")
537 					.requiring(badRequireModule, EnumSet.of(STATIC, TRANSITIVE), "101")
538 					.construct();
539 			fail();
540 		} catch(IllegalStateException e) {
541 			assertThat(e.getMessage(), containsString(badRequireModule));
542 		}
543 	}
544 
545 
546 	@Test
547 	public void requiresStaticToString()
548 	{
549 		String optionalModuleDep = "com.acme.optional.at.runtime";
550 		ModuleInfo m = ModuleInfo.moduleInfo()
551 				.named("com.acme.dodgy.arch")
552 				.requiring(optionalModuleDep, ACC_STATIC_PHASE, "101.404")
553 				.construct();
554 
555 		String expected = "module com.acme.dodgy.arch {\n" +
556 				"	/**\n" +
557 				"	 * @version 101.404\n" +
558 				"	 * @modifiers static" +
559 				"	 */\n" +
560 				"	requires static " + optionalModuleDep + ";\n" +
561 				"}\n";
562 
563 		assertThat(m, hasToString(equalToIgnoringWhiteSpace(expected)));
564 	}
565 
566 
567 	@Test
568 	public void requiresTransitiveToString()
569 	{
570 		String optionalModuleDep = "com.acme.ssh.do.not.tell.the.others";
571 		ModuleInfo m = ModuleInfo.moduleInfo()
572 				.named("com.acme.ostrich.depend.on.me")
573 				.requiring(optionalModuleDep, ACC_TRANSITIVE, "42.god")
574 				.construct();
575 
576 		String expected = "module com.acme.ostrich.depend.on.me {\n" +
577 				"	/**\n" +
578 				"	 * @version 42.god\n" +
579 				"	 * @modifiers transitive" +
580 				"	 */\n" +
581 				"	requires transitive " + optionalModuleDep + ";\n" +
582 				"}\n";
583 
584 		assertThat(m, hasToString(equalToIgnoringWhiteSpace(expected)));
585 	}
586 
587 
588 	@Test
589 	public void extractExisting() throws IOException
590 	{
591 		String moduleName = "hoohar";
592 
593 		ModuleInfo existing = moduleInfo()
594 				.named(moduleName)
595 				.exporting(pkg(DummyComparator.class))
596 				.construct();
597 
598 		InputStream built = TinyBundles.bundle()
599 				.symbolicName("not.relevant.right.now")
600 				.add(DummyComparator.class)
601 				.add("module-info.class", new ByteArrayInputStream(existing.toBytecode()))
602 				.build();
603 
604 		Path jar = Paths.get(".", "target", LocalDateTime.now(systemDefault()) + "_" + UUID.randomUUID() + ".jar");
605 		IoStreams.transfer(built, new FileOutputStream(jar.toFile()));
606 
607 		ModuleInfo extracted = ModuleInfo.extract(jar).orElseThrow(NullPointerException::new);
608 
609 		assertThat(extracted, is(equalTo(existing)));
610 	}
611 
612 
613 	@Test
614 	public void extractExistingFromInputStream() throws IOException
615 	{
616 		String moduleName = "hoohar";
617 
618 		ModuleInfo existing = moduleInfo()
619 				.named(moduleName)
620 				.exporting(pkg(DummyComparator.class))
621 				.construct();
622 
623 		InputStream built = TinyBundles.bundle()
624 				.symbolicName("not.relevant.right.now")
625 				.add(DummyComparator.class)
626 				.add("module-info.class", new ByteArrayInputStream(existing.toBytecode()))
627 				.build();
628 
629 		JarInputStream jar = new JarInputStream(built);
630 
631 		ModuleInfo extracted = ModuleInfo.extract(jar).orElseThrow(NullPointerException::new);
632 
633 		assertThat(extracted, is(equalTo(existing)));
634 	}
635 
636 
637 	@Test
638 	public void extractSynthetic() throws IOException
639 	{
640 		String moduleName = "hoohar";
641 
642 		InputStream built = TinyBundles.bundle()
643 				.symbolicName("not.relevant.right.now")
644 				.set("Automatic-Module-Name", moduleName)
645 				.add(DummyComparator.class)
646 				.add("META-INF/blah-blah/some.resource", new ByteArrayInputStream("blah blah".getBytes(UTF_8)))
647 				.add("NoPackageVeryBad.class", new FileInputStream("./target/test-classes/NoPackageVeryBad.class"))
648 				.build();
649 
650 		Path jar = Paths.get(".", "target", LocalDateTime.now(systemDefault()) + "_" + UUID.randomUUID() + ".jar");
651 		IoStreams.transfer(built, new FileOutputStream(jar.toFile()));
652 
653 		ModuleInfo moduleInfo = ModuleInfo.extract(jar).orElseThrow(NullPointerException::new);
654 
655 		assertThat(moduleInfo.name(), is(equalTo(moduleName)));
656 		assertThat(moduleInfo.provides(), is(anEmptyMap()));
657 		assertThat(moduleInfo.exports()
658 				.stream()
659 				.map(Export::paquet)
660 				.collect(toList()), contains(pkg(DummyComparator.class)));
661 
662 	}
663 
664 
665 	private String pkg(Class<?> type)
666 	{
667 		return type.getPackage().getName();
668 	}
669 
670 
671 	private String cn(Class<?> type)
672 	{
673 		return type.getCanonicalName();
674 	}
675 
676 
677 	@Test
678 	public void extractSyntheticWithMetaInfServices() throws IOException
679 	{
680 		String moduleName = "humbug";
681 
682 		InputStream built = TinyBundles.bundle()
683 				.symbolicName("not.relevant.right.now")
684 				.set("Automatic-Module-Name", moduleName)
685 				.add("META-INF/servicehistory", new ByteArrayInputStream("one careful owner".getBytes(UTF_8)))
686 				.add("META-INF/services/java.util.Comparator", new ByteArrayInputStream(cn(DummyComparator.class).getBytes(UTF_8)))
687 				.build();
688 
689 		Path jar = Paths.get(".", "target", LocalDateTime.now(systemDefault()) + "_" + UUID.randomUUID() + ".jar");
690 		IoStreams.transfer(built, new FileOutputStream(jar.toFile()));
691 
692 		ModuleInfo moduleInfo = ModuleInfo.extract(jar).orElseThrow(NullPointerException::new);
693 
694 		assertThat(moduleInfo.name(), is(equalTo(moduleName)));
695 		assertThat(moduleInfo.provides(), allOf(
696 				aMapWithSize(1),
697 				hasEntry(equalTo(cn(Comparator.class)), arrayContaining(cn(DummyComparator.class)))));
698 	}
699 
700 
701 	@Test
702 	public void extractNothing() throws IOException
703 	{
704 		InputStream built = TinyBundles.bundle()
705 				.symbolicName("not.relevant.right.now")
706 				.add("META-INF/services/java.util.Comparator", new ByteArrayInputStream(cn(DummyComparator.class).getBytes(UTF_8)))
707 				.build();
708 
709 		Path jar = Paths.get(".", "target", LocalDateTime.now(systemDefault()) + "_" + UUID.randomUUID() + ".jar");
710 		IoStreams.transfer(built, new FileOutputStream(jar.toFile()));
711 
712 		Optional<ModuleInfo> found = ModuleInfo.extract(jar);
713 
714 		assertThat(found.isPresent(), is(false));
715 	}
716 
717 
718 	@Test
719 	public void extractNothingFromExplodedArchive() throws IOException
720 	{
721 		Path jar = Paths.get(".", "target", LocalDateTime.now(systemDefault()) + "_" + UUID.randomUUID() + "_empty_exploded_jar");
722 		jar.toFile().mkdirs();
723 
724 		Optional<ModuleInfo> found = ModuleInfo.extract(jar);
725 
726 		assertThat(found.isPresent(), is(false));
727 	}
728 }