1418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file 2418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager// for details. All rights reserved. Use of this source code is governed by a 3418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager// BSD-style license that can be found in the LICENSE file. 4418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerpackage com.android.tools.r8.compatdx; 5418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 6418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport static org.junit.Assert.assertEquals; 7418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport static org.junit.Assert.assertNotEquals; 8418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport static org.junit.Assert.assertTrue; 9418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 10418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport com.android.tools.r8.ToolHelper; 11854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesseimport com.android.tools.r8.dex.Constants; 12418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport com.android.tools.r8.errors.CompilationError; 13418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport com.android.tools.r8.maindexlist.MainDexListTests; 14854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesseimport com.android.tools.r8.utils.AndroidApp; 15418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport com.android.tools.r8.utils.FileUtils; 16854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesseimport com.android.tools.r8.utils.OutputMode; 17418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport com.android.tools.r8.utils.StringUtils; 18418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport com.android.tools.r8.utils.StringUtils.BraceType; 19854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesseimport com.google.common.collect.ImmutableList; 20418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.io.IOException; 21418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.nio.file.Files; 22418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.nio.file.Path; 23418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.nio.file.Paths; 24418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.util.ArrayList; 25418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.util.Collections; 26418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.util.List; 27418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.util.Set; 28854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesseimport java.util.concurrent.ExecutionException; 29418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.util.stream.Collectors; 30418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.util.zip.ZipEntry; 31418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport java.util.zip.ZipFile; 32418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport org.junit.Rule; 33418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport org.junit.Test; 34418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport org.junit.rules.ExpectedException; 35418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerimport org.junit.rules.TemporaryFolder; 36418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 37418d1ca139ea11316113beafbb3b3dd3fd5587aMads Agerpublic class CompatDxTests { 38418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private static final String EXAMPLE_JAR_FILE1 = "build/test/examples/arithmetic.jar"; 39418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private static final String EXAMPLE_JAR_FILE2 = "build/test/examples/barray.jar"; 40418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 41418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private static final String NO_LOCALS = "--no-locals"; 42418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private static final String NO_POSITIONS = "--positions=none"; 43418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private static final String MULTIDEX = "--multi-dex"; 44418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private static final String NUM_THREADS_5 = "--num-threads=5"; 45418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 46418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Rule 47418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public ExpectedException thrown = ExpectedException.none(); 48418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 49418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Rule 50418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest(); 51418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 52418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 53418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void noFilesTest() throws IOException { 54418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexer("--no-files"); 55418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 56418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 57418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 58418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void noOutputTest() throws IOException { 59418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexerWithoutOutput(NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1); 60418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 61418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 62418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 63418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void singleJarInputFile() throws IOException { 64418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexer(NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1); 65418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 66418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 67418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 68418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void multipleJarInputFiles() throws IOException { 69418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexer(NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1, EXAMPLE_JAR_FILE2); 70418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 71418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 72418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 73418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void outputZipFile() throws IOException { 74418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexerWithOutput("foo.dex.zip", NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1); 75418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 76418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 77418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 78418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void useMultipleThreads() throws IOException { 79418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexer(NUM_THREADS_5, NO_POSITIONS, NO_LOCALS, EXAMPLE_JAR_FILE1); 80418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 81418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 82418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 83418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void withPositions() throws IOException { 84418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexer(NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1); 85418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 86418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 87418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 88418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void withLocals() throws IOException { 89418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexer(NO_POSITIONS, MULTIDEX, EXAMPLE_JAR_FILE1); 90418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 91418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 92418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 93418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void withoutMultidex() throws IOException { 94418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexer(NO_POSITIONS, NO_LOCALS, EXAMPLE_JAR_FILE1); 95418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 96418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 97418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 98418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager public void writeToNamedDexFile() throws IOException { 99418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexerWithOutput("named-output.dex", EXAMPLE_JAR_FILE1); 100418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 101418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 102418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager @Test 10302cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic public void singleDexProgramFull() throws IOException, ExecutionException { 10402cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic // Generate an application that fills the whole dex file. 10502cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic AndroidApp generated = 10602cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic MainDexListTests.generateApplication( 10702cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic ImmutableList.of("A"), Constants.ANDROID_L_API, Constants.U16BIT_MAX + 1); 10802cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic Path applicationJar = temp.newFile("application.jar").toPath(); 10902cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic generated.write(applicationJar, OutputMode.Indexed); 11002cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic runDexer(applicationJar.toString()); 11102cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic } 11202cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic 11302cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic @Test 114854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesse public void singleDexProgramIsTooLarge() throws IOException, ExecutionException { 115854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesse // Generate an application that will not fit into a single dex file. 116854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesse AndroidApp generated = MainDexListTests.generateApplication( 11702cc4ebce7deb01a5509624f70e7d667d5f4e2c8Ivan Gavrilovic ImmutableList.of("A", "B"), Constants.ANDROID_L_API, Constants.U16BIT_MAX / 2 + 2); 118854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesse Path applicationJar = temp.newFile("application.jar").toPath(); 119341bbdae4c7b6fe0f5dd95c7158488af6768e7baIan Zerny generated.write(applicationJar, OutputMode.Indexed); 120418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager thrown.expect(CompilationError.class); 121854a7d4617a50c95bdbd7efc266c7ba6335b8d0cSøren Gjesse runDexer(applicationJar.toString()); 122418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 123418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 12454af84519a2116bb70618c65631fcf06b47d729bIan Zerny @Test 12554af84519a2116bb70618c65631fcf06b47d729bIan Zerny public void keepClassesTest() throws IOException { 12654af84519a2116bb70618c65631fcf06b47d729bIan Zerny runDexerWithOutput("out.zip", "--keep-classes", EXAMPLE_JAR_FILE1); 12754af84519a2116bb70618c65631fcf06b47d729bIan Zerny } 12854af84519a2116bb70618c65631fcf06b47d729bIan Zerny 129418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private void runDexer(String... args) throws IOException { 130418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexerWithOutput("", args); 131418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 132418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 133418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private void runDexerWithoutOutput(String... args) throws IOException { 134418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager runDexerWithOutput(null, args); 135418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 136418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 13754af84519a2116bb70618c65631fcf06b47d729bIan Zerny private Path getOutputD8() { 13854af84519a2116bb70618c65631fcf06b47d729bIan Zerny return temp.getRoot().toPath().resolve("d8-out"); 13954af84519a2116bb70618c65631fcf06b47d729bIan Zerny } 14054af84519a2116bb70618c65631fcf06b47d729bIan Zerny 14154af84519a2116bb70618c65631fcf06b47d729bIan Zerny private Path getOutputDX() { 14254af84519a2116bb70618c65631fcf06b47d729bIan Zerny return temp.getRoot().toPath().resolve("dx-out"); 14354af84519a2116bb70618c65631fcf06b47d729bIan Zerny } 14454af84519a2116bb70618c65631fcf06b47d729bIan Zerny 145418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private void runDexerWithOutput(String out, String... args) throws IOException { 146418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager Path d8Out = null; 147418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager Path dxOut = null; 148418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager if (out != null) { 14954af84519a2116bb70618c65631fcf06b47d729bIan Zerny Path baseD8 = getOutputD8(); 15054af84519a2116bb70618c65631fcf06b47d729bIan Zerny Path baseDX = getOutputDX(); 15154af84519a2116bb70618c65631fcf06b47d729bIan Zerny Files.createDirectory(baseD8); 15254af84519a2116bb70618c65631fcf06b47d729bIan Zerny Files.createDirectory(baseDX); 15354af84519a2116bb70618c65631fcf06b47d729bIan Zerny d8Out = baseD8.resolve(out); 15454af84519a2116bb70618c65631fcf06b47d729bIan Zerny dxOut = baseDX.resolve(out); 155418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager assertNotEquals(d8Out, dxOut); 156418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 157418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 158418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager List<String> d8Args = new ArrayList<>(args.length + 2); 159418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager d8Args.add("--dex"); 160418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager if (d8Out != null) { 161418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager d8Args.add("--output=" + d8Out); 162418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 163418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager Collections.addAll(d8Args, args); 164418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager System.out.println("running: d8 " + StringUtils.join(d8Args, " ")); 165418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager CompatDx.main(d8Args.toArray(new String[d8Args.size()])); 166418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 167418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager List<String> dxArgs = new ArrayList<>(args.length + 2); 168418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager if (dxOut != null) { 169418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager dxArgs.add("--output=" + dxOut); 170418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 171418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager Collections.addAll(dxArgs, args); 172418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager System.out.println("running: dx " + StringUtils.join(dxArgs, " ")); 173418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager ToolHelper.runDX(dxArgs.toArray(new String[dxArgs.size()])); 174418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 175418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager if (out == null) { 176418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager // Can't check output if explicitly not writing any. 177418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager return; 178418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 179418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 180418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager List<Path> d8Files = Files.list(Files.isDirectory(d8Out) ? d8Out : d8Out.getParent()) 181418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager .sorted().collect(Collectors.toList()); 182418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager List<Path> dxFiles = Files.list(Files.isDirectory(dxOut) ? dxOut : dxOut.getParent()) 183418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager .sorted().collect(Collectors.toList()); 184418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager assertEquals("Out file names differ", 185418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager StringUtils.join(dxFiles, "\n", BraceType.NONE, (file) -> 186418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager file.getFileName().toString()), 187418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager StringUtils.join(d8Files, "\n", BraceType.NONE, (file) -> 188418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager file.getFileName().toString())); 189418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 190418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager for (int i = 0; i < d8Files.size(); i++) { 191418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager if (FileUtils.isArchive(d8Files.get(i))) { 192418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager compareArchiveFiles(d8Files.get(i), dxFiles.get(i)); 193418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 194418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 195418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 196418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager 197418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager private void compareArchiveFiles(Path d8File, Path dxFile) throws IOException { 198418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager ZipFile d8Zip = new ZipFile(d8File.toFile()); 199418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager ZipFile dxZip = new ZipFile(dxFile.toFile()); 200418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager // TODO(zerny): This should test resource containment too once supported. 201418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager Set<String> d8Content = d8Zip.stream().map(ZipEntry::getName).collect(Collectors.toSet()); 202418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager Set<String> dxContent = dxZip.stream().map(ZipEntry::getName).collect(Collectors.toSet()); 203418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager for (String entry : d8Content) { 204418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager assertTrue("Expected dx output to contain " + entry, dxContent.contains(entry)); 205418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 206418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager for (String entry : dxContent) { 20754af84519a2116bb70618c65631fcf06b47d729bIan Zerny Path path = Paths.get(entry); 20854af84519a2116bb70618c65631fcf06b47d729bIan Zerny if (FileUtils.isDexFile(path) || FileUtils.isClassFile(path)) { 209418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager assertTrue("Expected d8 output to contain " + entry, d8Content.contains(entry)); 210418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 211418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 212418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager } 213418d1ca139ea11316113beafbb3b3dd3fd5587aMads Ager} 214