1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
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 }