From c5cab11622068134afb0421721b897ad5dbefe10 Mon Sep 17 00:00:00 2001 From: Jia_RG Date: Sun, 2 Apr 2023 15:42:39 +0800 Subject: [PATCH 1/6] Fix: fix SysGenProfilingFile rename fail --- .../myperf4j/core/MethodMetricsHistogram.java | 26 ++- .../test/java/cn/myperf4j/core/FileTest.java | 161 ++++++++++++++++++ 2 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java diff --git a/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java b/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java index e94002e..d0865f6 100644 --- a/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java +++ b/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java @@ -9,6 +9,10 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -73,23 +77,29 @@ public static void buildSysGenProfilingFile() { if (!neverInvokedMethods.isEmpty()) { fileWriter.write("#The following methods have never been invoked!\n"); - for (int i = 0; i < neverInvokedMethods.size(); i++) { - final Integer methodId = neverInvokedMethods.get(i); + for (final Integer methodId : neverInvokedMethods) { writeProfilingInfo(tagMaintainer, fileWriter, methodId, 128); } fileWriter.flush(); } + } catch (Exception e) { + Logger.error("MethodMetricssHistogram.buildSysGenProfilingFile() error", e); + } - final File destFile = new File(filePath); - final boolean rename = tempFile.renameTo(destFile) && destFile.setReadOnly(); + try { + final Path tempPath = tempFile.toPath(); + final Path moved = Files.move(tempPath, Paths.get(filePath), StandardCopyOption.REPLACE_EXISTING); + final File destFile = moved.toFile(); + // 此处不能设置只读,否则在windows环境下次move的时候会报错 AccessDeniedException + final boolean rename = destFile.exists()/* && destFile.setReadOnly() */; Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile(): rename " + tempFile.getName() + " to " + destFile.getName() + " " + (rename ? "success" : "fail")); } catch (Exception e) { - Logger.error("MethodMetricsHistogram.buildSysGenProfilingFile()", e); - } finally { - Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile() finished, cost=" - + (System.currentTimeMillis() - startMills) + "ms"); + Logger.error("MethodMetricsHistogram.buildSysGenProfilingFile() rename error", e); } + + Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile() finished, cost=" + + (System.currentTimeMillis() - startMills) + "ms"); } private static void writeProfilingInfo(MethodTagMaintainer tagMaintainer, diff --git a/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java b/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java new file mode 100644 index 0000000..3658551 --- /dev/null +++ b/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java @@ -0,0 +1,161 @@ +package cn.myperf4j.core; + +import cn.myperf4j.base.util.Logger; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Objects; + +/** + * 文件测试 + */ +public class FileTest { + + protected static final File baseDir = new File(System.getProperty("user.dir")); + + protected static final File testDir = new File(baseDir, "temp"); + + @BeforeClass + public static void prepare() { + getDir(FileTest.class.getSimpleName()); + } + + @AfterClass + public static void cleanup() { + deleteDir(FileTest.class.getSimpleName()); + } + + + @Test + public void testFileRename() { + String testFile = getDir().getPath() + "/testFileRename"; + final Path testFilePath1 = Paths.get(testFile + 1); + final Path testFilePath2 = Paths.get(testFile + 2); + final Path tmpFilePath = Paths.get(testFile + "_tmp"); + + boolean firstRenameTo = false; + boolean firstReadOnly = false; + + boolean renameToResult = false; + boolean readOnlyResult = false; + + for (int i = 0; i < 2; i++) { + try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(tmpFilePath.toFile(), false), 8192)) { + fileWriter.write("#This is a file automatically generated by MyPerf4J, please do not edit!\n"); + fileWriter.flush(); + final boolean renameTo = tmpFilePath.toFile().renameTo(testFilePath1.toFile()); + final boolean readOnly = testFilePath1.toFile().setReadOnly(); + // 流还没释放导致 + Assert.assertFalse(renameTo); + Assert.assertFalse(readOnly); + } catch (Exception e) { + Logger.error("testFileRename error", e); + } + + if (i == 0) { + firstRenameTo = tmpFilePath.toFile().renameTo(testFilePath2.toFile()); + firstReadOnly = testFilePath2.toFile().setReadOnly(); + } else { + renameToResult = tmpFilePath.toFile().renameTo(testFilePath2.toFile()); + readOnlyResult = testFilePath2.toFile().setReadOnly(); + } + } + // tmp文件存在 + Assert.assertTrue(tmpFilePath.toFile().exists()); + // BufferedWriter没释放时rename的文件不存在 + Assert.assertFalse(testFilePath1.toFile().exists()); + // BufferedWriter释放后rename的文件存在 + Assert.assertTrue(testFilePath2.toFile().exists()); + + // 第一次rename能成功 文件存在的情况setReadOnly始终能成功 + Assert.assertTrue(firstRenameTo); + Assert.assertTrue(firstReadOnly); + Assert.assertTrue(readOnlyResult); + + // 第一次rename能成功,后续就失败 + Assert.assertFalse(renameToResult); + } + + @Test + public void testFilesMove() throws IOException { + String testFile = getDir().getPath() + "/testFilesMove"; + final Path testFilePath = Paths.get(testFile); + final Path tmpFilePath = Paths.get(testFile + "_tmp"); + if (!tmpFilePath.toFile().exists()) { + Files.createFile(tmpFilePath); + tmpFilePath.toFile().setReadOnly(); + } + + Assert.assertFalse(tmpFilePath.toFile().canWrite()); + + final Path moved = Files.move(tmpFilePath, testFilePath, StandardCopyOption.REPLACE_EXISTING); + + moved.toFile().setReadOnly(); + + Assert.assertFalse(moved.toFile().canWrite()); + + Files.move(moved, tmpFilePath, StandardCopyOption.REPLACE_EXISTING); + + Assert.assertFalse(tmpFilePath.toFile().canWrite()); + } + + public File getDir() { + return getDir(getName()); + } + + public String getName() { + return getClass().getSimpleName(); + } + + public static File getDir(String dir) { + return getDir(testDir, dir); + } + + public static File getDir(File parent, String dir) { + File file = new File(parent, dir); + if (!file.isDirectory()) { + if (!file.mkdirs()) { + throw new IllegalStateException("Can't create dir: " + file); + } + } + return file; + } + + public static void deleteDir(String dir) { + deleteDir(testDir, dir); + } + + public static void deleteDir(final File parent, String dir) { + final File dirFile = new File(parent, dir); + if (dirFile.isDirectory()) { + for (final File f: Objects.requireNonNull(dirFile.listFiles())) { + if (f.isDirectory()) { + deleteDir(dirFile, f.getName()); + continue; + } + if (f.isFile()){ + if (!f.delete()) { + throw new IllegalStateException("Can't delete file: " + f); + } + Logger.info("Delete file: " + f); + } + } + if (!dirFile.delete()) { + throw new IllegalStateException("Can't delete directory: " + dirFile); + } + Logger.info("Delete directory: " + dirFile); + } + testDir.deleteOnExit(); + Logger.info("Delete testDir: " + testDir); + } +} From c41c1aa33c12bbd5928e2efe0ea0280c47f9c10a Mon Sep 17 00:00:00 2001 From: Jia_RG Date: Sun, 2 Apr 2023 22:28:34 +0800 Subject: [PATCH 2/6] Refactor: code review --- .../myperf4j/core/MethodMetricsHistogram.java | 39 +++++++------ .../test/java/cn/myperf4j/core/FileTest.java | 57 ++++++------------- 2 files changed, 38 insertions(+), 58 deletions(-) diff --git a/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java b/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java index d0865f6..4ee1495 100644 --- a/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java +++ b/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java @@ -55,8 +55,27 @@ private static void recordMetrics0(int methodTagId, int tp95, int tp99, int tp99 public static void buildSysGenProfilingFile() { final long startMills = System.currentTimeMillis(); final String filePath = ProfilingConfig.basicConfig().sysProfilingParamsFile(); + + try { + final File tempFile = buildTmpProfilingFile(filePath); + final Path tempPath = tempFile.toPath(); + final Path destPath = Files.move(tempPath, Paths.get(filePath), StandardCopyOption.REPLACE_EXISTING); + final File destFile = destPath.toFile(); + // 此处不能设置只读,否则在windows环境下次move的时候会报错 AccessDeniedException + final boolean rename = destFile.exists()/* && destFile.setReadOnly() */; + Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile(): rename " + tempFile.getName() + + " to " + destFile.getName() + " " + (rename ? "success" : "fail")); + } catch (Exception e) { + Logger.error("MethodMetricsHistogram.buildSysGenProfilingFile() rename error", e); + } finally { + Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile() finished, cost=" + + (System.currentTimeMillis() - startMills) + "ms"); + } + } + + private static File buildTmpProfilingFile(String filePath) throws IOException { final String tempFilePath = filePath + "_tmp"; - final File tempFile = new File(tempFilePath); + final File tempFile = Paths.get(tempFilePath).toFile(); try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(tempFile, false), 8 * 1024)) { fileWriter.write("#This is a file automatically generated by MyPerf4J, please do not edit!\n"); @@ -82,24 +101,8 @@ public static void buildSysGenProfilingFile() { } fileWriter.flush(); } - } catch (Exception e) { - Logger.error("MethodMetricssHistogram.buildSysGenProfilingFile() error", e); - } - - try { - final Path tempPath = tempFile.toPath(); - final Path moved = Files.move(tempPath, Paths.get(filePath), StandardCopyOption.REPLACE_EXISTING); - final File destFile = moved.toFile(); - // 此处不能设置只读,否则在windows环境下次move的时候会报错 AccessDeniedException - final boolean rename = destFile.exists()/* && destFile.setReadOnly() */; - Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile(): rename " + tempFile.getName() - + " to " + destFile.getName() + " " + (rename ? "success" : "fail")); - } catch (Exception e) { - Logger.error("MethodMetricsHistogram.buildSysGenProfilingFile() rename error", e); } - - Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile() finished, cost=" - + (System.currentTimeMillis() - startMills) + "ms"); + return tempFile; } private static void writeProfilingInfo(MethodTagMaintainer tagMaintainer, diff --git a/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java b/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java index 3658551..e678df0 100644 --- a/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java +++ b/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java @@ -21,9 +21,9 @@ */ public class FileTest { - protected static final File baseDir = new File(System.getProperty("user.dir")); + private static final File baseDir = new File(System.getProperty("user.dir")); - protected static final File testDir = new File(baseDir, "temp"); + private static final File testDir = new File(baseDir, "temp"); @BeforeClass public static void prepare() { @@ -35,7 +35,6 @@ public static void cleanup() { deleteDir(FileTest.class.getSimpleName()); } - @Test public void testFileRename() { String testFile = getDir().getPath() + "/testFileRename"; @@ -43,31 +42,25 @@ public void testFileRename() { final Path testFilePath2 = Paths.get(testFile + 2); final Path tmpFilePath = Paths.get(testFile + "_tmp"); - boolean firstRenameTo = false; - boolean firstReadOnly = false; - - boolean renameToResult = false; - boolean readOnlyResult = false; - for (int i = 0; i < 2; i++) { try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(tmpFilePath.toFile(), false), 8192)) { fileWriter.write("#This is a file automatically generated by MyPerf4J, please do not edit!\n"); fileWriter.flush(); - final boolean renameTo = tmpFilePath.toFile().renameTo(testFilePath1.toFile()); - final boolean readOnly = testFilePath1.toFile().setReadOnly(); // 流还没释放导致 - Assert.assertFalse(renameTo); - Assert.assertFalse(readOnly); + Assert.assertFalse(tmpFilePath.toFile().renameTo(testFilePath1.toFile())); + Assert.assertFalse(testFilePath1.toFile().setReadOnly()); } catch (Exception e) { Logger.error("testFileRename error", e); } if (i == 0) { - firstRenameTo = tmpFilePath.toFile().renameTo(testFilePath2.toFile()); - firstReadOnly = testFilePath2.toFile().setReadOnly(); + // 第一次rename能成功 文件存在的情况setReadOnly始终能成功 + Assert.assertTrue(tmpFilePath.toFile().renameTo(testFilePath2.toFile())); + Assert.assertTrue(testFilePath2.toFile().setReadOnly()); } else { - renameToResult = tmpFilePath.toFile().renameTo(testFilePath2.toFile()); - readOnlyResult = testFilePath2.toFile().setReadOnly(); + // 第一次rename能成功,后续就失败 + Assert.assertFalse(tmpFilePath.toFile().renameTo(testFilePath2.toFile())); + Assert.assertTrue(testFilePath2.toFile().setReadOnly()); } } // tmp文件存在 @@ -76,14 +69,6 @@ public void testFileRename() { Assert.assertFalse(testFilePath1.toFile().exists()); // BufferedWriter释放后rename的文件存在 Assert.assertTrue(testFilePath2.toFile().exists()); - - // 第一次rename能成功 文件存在的情况setReadOnly始终能成功 - Assert.assertTrue(firstRenameTo); - Assert.assertTrue(firstReadOnly); - Assert.assertTrue(readOnlyResult); - - // 第一次rename能成功,后续就失败 - Assert.assertFalse(renameToResult); } @Test @@ -95,34 +80,26 @@ public void testFilesMove() throws IOException { Files.createFile(tmpFilePath); tmpFilePath.toFile().setReadOnly(); } - Assert.assertFalse(tmpFilePath.toFile().canWrite()); final Path moved = Files.move(tmpFilePath, testFilePath, StandardCopyOption.REPLACE_EXISTING); - moved.toFile().setReadOnly(); - Assert.assertFalse(moved.toFile().canWrite()); Files.move(moved, tmpFilePath, StandardCopyOption.REPLACE_EXISTING); - Assert.assertFalse(tmpFilePath.toFile().canWrite()); } - public File getDir() { + private File getDir() { return getDir(getName()); } - public String getName() { + private String getName() { return getClass().getSimpleName(); } - public static File getDir(String dir) { - return getDir(testDir, dir); - } - - public static File getDir(File parent, String dir) { - File file = new File(parent, dir); + private static File getDir(String dir) { + File file = new File(testDir, dir); if (!file.isDirectory()) { if (!file.mkdirs()) { throw new IllegalStateException("Can't create dir: " + file); @@ -131,11 +108,11 @@ public static File getDir(File parent, String dir) { return file; } - public static void deleteDir(String dir) { + private static void deleteDir(String dir) { deleteDir(testDir, dir); } - public static void deleteDir(final File parent, String dir) { + private static void deleteDir(final File parent, String dir) { final File dirFile = new File(parent, dir); if (dirFile.isDirectory()) { for (final File f: Objects.requireNonNull(dirFile.listFiles())) { @@ -143,7 +120,7 @@ public static void deleteDir(final File parent, String dir) { deleteDir(dirFile, f.getName()); continue; } - if (f.isFile()){ + if (f.isFile()) { if (!f.delete()) { throw new IllegalStateException("Can't delete file: " + f); } From 8cf7cd1bc9d78b8a87a599441ce113204c9881d6 Mon Sep 17 00:00:00 2001 From: Jia_RG Date: Sun, 2 Apr 2023 23:51:56 +0800 Subject: [PATCH 3/6] Style: remove mvn checkstyle warnings --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f8a3458..2bb2730 100644 --- a/pom.xml +++ b/pom.xml @@ -148,7 +148,7 @@ com.puppycrawl.tools checkstyle - 8.29 + 8.40 io.netty From 3f2ba935fb70378c099db367e9cbfaab5343c21e Mon Sep 17 00:00:00 2001 From: Jia_RG Date: Mon, 3 Apr 2023 01:26:19 +0800 Subject: [PATCH 4/6] Style: remove deceptive log --- .../src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java b/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java index 4ee1495..a12769c 100644 --- a/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java +++ b/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java @@ -66,7 +66,7 @@ public static void buildSysGenProfilingFile() { Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile(): rename " + tempFile.getName() + " to " + destFile.getName() + " " + (rename ? "success" : "fail")); } catch (Exception e) { - Logger.error("MethodMetricsHistogram.buildSysGenProfilingFile() rename error", e); + Logger.error("MethodMetricsHistogram.buildSysGenProfilingFile() error", e); } finally { Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile() finished, cost=" + (System.currentTimeMillis() - startMills) + "ms"); From 17c9823267059a6931ce205e37c6f3b0e474ab44 Mon Sep 17 00:00:00 2001 From: Jia_RG Date: Mon, 3 Apr 2023 15:47:23 +0800 Subject: [PATCH 5/6] Refactor: ConfigKey toString --- .../src/main/java/cn/myperf4j/base/config/ConfigKey.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MyPerf4J-Base/src/main/java/cn/myperf4j/base/config/ConfigKey.java b/MyPerf4J-Base/src/main/java/cn/myperf4j/base/config/ConfigKey.java index 614e95f..b414f3a 100644 --- a/MyPerf4J-Base/src/main/java/cn/myperf4j/base/config/ConfigKey.java +++ b/MyPerf4J-Base/src/main/java/cn/myperf4j/base/config/ConfigKey.java @@ -22,6 +22,11 @@ public String legacyKey() { return legacyKey; } + @Override + public String toString() { + return key; + } + public static ConfigKey of(String key, String legacyKey) { return new ConfigKey(key, legacyKey); } From b8ba3b10a6ccd31741233cc9513fd2fb39cabd2e Mon Sep 17 00:00:00 2001 From: Jia_RG Date: Mon, 10 Apr 2023 20:52:09 +0800 Subject: [PATCH 6/6] Refactor: code review --- .../cn/myperf4j/core/MethodMetricsHistogram.java | 6 +++--- .../src/test/java/cn/myperf4j/core/FileTest.java | 15 +++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java b/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java index a12769c..e098c64 100644 --- a/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java +++ b/MyPerf4J-Core/src/main/java/cn/myperf4j/core/MethodMetricsHistogram.java @@ -12,13 +12,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + /** * Created by LinShunkang on 2019/07/23 */ @@ -58,8 +59,7 @@ public static void buildSysGenProfilingFile() { try { final File tempFile = buildTmpProfilingFile(filePath); - final Path tempPath = tempFile.toPath(); - final Path destPath = Files.move(tempPath, Paths.get(filePath), StandardCopyOption.REPLACE_EXISTING); + final Path destPath = Files.move(tempFile.toPath(), Paths.get(filePath), REPLACE_EXISTING); final File destFile = destPath.toFile(); // 此处不能设置只读,否则在windows环境下次move的时候会报错 AccessDeniedException final boolean rename = destFile.exists()/* && destFile.setReadOnly() */; diff --git a/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java b/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java index e678df0..7406002 100644 --- a/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java +++ b/MyPerf4J-Core/src/test/java/cn/myperf4j/core/FileTest.java @@ -37,7 +37,7 @@ public static void cleanup() { @Test public void testFileRename() { - String testFile = getDir().getPath() + "/testFileRename"; + String testFile = getTestDir().getPath() + "/testFileRename"; final Path testFilePath1 = Paths.get(testFile + 1); final Path testFilePath2 = Paths.get(testFile + 2); final Path tmpFilePath = Paths.get(testFile + "_tmp"); @@ -73,7 +73,7 @@ public void testFileRename() { @Test public void testFilesMove() throws IOException { - String testFile = getDir().getPath() + "/testFilesMove"; + String testFile = getTestDir().getPath() + "/testFilesMove"; final Path testFilePath = Paths.get(testFile); final Path tmpFilePath = Paths.get(testFile + "_tmp"); if (!tmpFilePath.toFile().exists()) { @@ -90,12 +90,8 @@ public void testFilesMove() throws IOException { Assert.assertFalse(tmpFilePath.toFile().canWrite()); } - private File getDir() { - return getDir(getName()); - } - - private String getName() { - return getClass().getSimpleName(); + private File getTestDir() { + return getDir(FileTest.class.getSimpleName()); } private static File getDir(String dir) { @@ -120,6 +116,7 @@ private static void deleteDir(final File parent, String dir) { deleteDir(dirFile, f.getName()); continue; } + if (f.isFile()) { if (!f.delete()) { throw new IllegalStateException("Can't delete file: " + f); @@ -127,11 +124,13 @@ private static void deleteDir(final File parent, String dir) { Logger.info("Delete file: " + f); } } + if (!dirFile.delete()) { throw new IllegalStateException("Can't delete directory: " + dirFile); } Logger.info("Delete directory: " + dirFile); } + testDir.deleteOnExit(); Logger.info("Delete testDir: " + testDir); }