]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20268 Add new module for test failure monitoring
authorSteve Marion <unknown>
Wed, 30 Aug 2023 13:27:35 +0000 (15:27 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 5 Sep 2023 20:02:53 +0000 (20:02 +0000)
build.gradle
settings.gradle
test-monitoring/build.gradle [new file with mode: 0644]
test-monitoring/src/main/java/org/sonarqube/monitoring/test/Measure.java [new file with mode: 0644]
test-monitoring/src/main/java/org/sonarqube/monitoring/test/aspect/TestFailureAspect.java [new file with mode: 0644]
test-monitoring/src/main/java/org/sonarqube/monitoring/test/aspect/package-info.java [new file with mode: 0644]
test-monitoring/src/main/java/org/sonarqube/monitoring/test/package-info.java [new file with mode: 0644]
test-monitoring/src/main/resources/META-INF/aop.xml [new file with mode: 0644]
test-monitoring/src/test/java/org/sonarqube/monitoring/test/aspect/TestFailureAspectTest.java [new file with mode: 0644]
wss-unified-agent.config

index 9b7c402794438052eb684412cf2b1ddca0c26eee..90234880d1a858613b5b4944b814050937e3dea3 100644 (file)
@@ -65,6 +65,7 @@ allprojects {
   ext {
     release = project.hasProperty('release') && project.getProperty('release')
     official = project.hasProperty('official') && project.getProperty('official')
+    withTestMonitoring = project.hasProperty('withTestMonitoring')
   }
 
   ext.enableBom = enableBom
@@ -637,12 +638,15 @@ subprojects {
   tasks.withType(Test) {
     configurations {
       utMonitoring
+      testMonitoring
     }
 
     dependencies {
       testImplementation project(":ut-monitoring")
+      testImplementation project(":test-monitoring")
 
       utMonitoring 'org.aspectj:aspectjweaver:1.9.19'
+      testMonitoring 'org.aspectj:aspectjweaver:1.9.19'
     }
   }
 
@@ -678,6 +682,17 @@ subprojects {
     }
   }
 
+  if (ext.withTestMonitoring) {
+    tasks.withType(Test) {
+      doFirst {
+        ext {
+          aspectJWeaver = configurations.testMonitoring.resolvedConfiguration.resolvedArtifacts.find { it.name == 'aspectjweaver' }
+        }
+        jvmArgs "-javaagent:${aspectJWeaver.file}"
+      }
+    }
+  }
+
 
   signing {
     def signingKeyId = findProperty("signingKeyId")
index 912592cff2b52fdc72a091605971a6c406ab0cd3..895b5c8c6770a271dce6e50737ec268ac6cbb125 100644 (file)
@@ -59,6 +59,7 @@ include 'sonar-testing-harness'
 include 'sonar-testing-ldap'
 include 'sonar-ws'
 include 'sonar-ws-generator'
+include 'test-monitoring'
 include 'ut-monitoring'
 
 ext.isCiServer = System.getenv().containsKey("CIRRUS_CI")
diff --git a/test-monitoring/build.gradle b/test-monitoring/build.gradle
new file mode 100644 (file)
index 0000000..236f8a3
--- /dev/null
@@ -0,0 +1,26 @@
+sonar {
+    properties {
+        property 'sonar.projectName', "${projectTitle} :: Java Tests Monitoring"
+    }
+}
+
+dependencies {
+    // please keep the list grouped by configuration and ordered by name
+
+    api 'com.google.code.gson:gson'
+    api 'junit:junit'
+
+    compileOnlyApi 'org.aspectj:aspectjtools'
+    compileOnlyApi 'com.google.code.findbugs:jsr305'
+
+    testImplementation 'org.assertj:assertj-core'
+    testImplementation 'org.mockito:mockito-core'
+}
+
+tasks.withType(JavaCompile) {
+    options.release = 11
+}
+
+sonar {
+    skipProject = true
+}
diff --git a/test-monitoring/src/main/java/org/sonarqube/monitoring/test/Measure.java b/test-monitoring/src/main/java/org/sonarqube/monitoring/test/Measure.java
new file mode 100644 (file)
index 0000000..efefa0f
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.monitoring.test;
+
+public class Measure {
+  private String timestamp;
+  private String branchName;
+  private String commit;
+  private String build;
+  private String category;
+  private String testClass;
+  private String testMethod;
+  private String exceptionClass;
+  private String exceptionMessage;
+  private String exceptionLogs;
+
+  private Measure() {
+  }
+
+  public String getTimestamp() {
+    return timestamp;
+  }
+
+  public void setTimestamp(String timestamp) {
+    this.timestamp = timestamp;
+  }
+
+  public String getBranchName() {
+    return branchName;
+  }
+
+  public void setBranchName(String branchName) {
+    this.branchName = branchName;
+  }
+
+  public String getCommit() {
+    return commit;
+  }
+
+  public void setCommit(String commit) {
+    this.commit = commit;
+  }
+
+  public String getBuild() {
+    return build;
+  }
+
+  public void setBuild(String build) {
+    this.build = build;
+  }
+
+  public String getCategory() {
+    return category;
+  }
+
+  public void setCategory(String category) {
+    this.category = category;
+  }
+
+  public String getTestClass() {
+    return testClass;
+  }
+
+  public void setTestClass(String testClass) {
+    this.testClass = testClass;
+  }
+
+  public String getTestMethod() {
+    return testMethod;
+  }
+
+  public void setTestMethod(String testMethod) {
+    this.testMethod = testMethod;
+  }
+
+  public String getExceptionClass() {
+    return exceptionClass;
+  }
+
+  public void setExceptionClass(String exceptionClass) {
+    this.exceptionClass = exceptionClass;
+  }
+
+  public String getExceptionMessage() {
+    return exceptionMessage;
+  }
+
+  public void setExceptionMessage(String exceptionMessage) {
+    this.exceptionMessage = exceptionMessage;
+  }
+
+  public String getExceptionLogs() {
+    return exceptionLogs;
+  }
+
+  public void setExceptionLogs(String exceptionLogs) {
+    this.exceptionLogs = exceptionLogs;
+  }
+
+  @Override
+  public String toString() {
+    return "Measure{" +
+      "timestamp='" + timestamp + '\'' +
+      ", branchName='" + branchName + '\'' +
+      ", commit='" + commit + '\'' +
+      ", build='" + build + '\'' +
+      ", category='" + category + '\'' +
+      ", testClass='" + testClass + '\'' +
+      ", testMethod='" + testMethod + '\'' +
+      ", exceptionClass='" + exceptionClass + '\'' +
+      ", exceptionMessage='" + exceptionMessage + '\'' +
+      ", exceptionLogs='" + exceptionLogs + '\'' +
+      '}';
+  }
+
+  public static final class MeasureBuilder {
+    private Measure measure;
+
+    private MeasureBuilder() {
+      measure = new Measure();
+    }
+
+    public static MeasureBuilder newMeasureBuilder() {
+      return new MeasureBuilder();
+    }
+
+    public MeasureBuilder setTimestamp(String timestamp) {
+      measure.setTimestamp(timestamp);
+      return this;
+    }
+
+    public MeasureBuilder setBranchName(String branchName) {
+      measure.setBranchName(branchName);
+      return this;
+    }
+
+    public MeasureBuilder setCommit(String commit) {
+      measure.setCommit(commit);
+      return this;
+    }
+
+    public MeasureBuilder setBuild(String build) {
+      measure.setBuild(build);
+      return this;
+    }
+
+    public MeasureBuilder setCategory(String category) {
+      measure.setCategory(category);
+      return this;
+    }
+
+    public MeasureBuilder setTestClass(String testClass) {
+      measure.setTestClass(testClass);
+      return this;
+    }
+
+    public MeasureBuilder setTestMethod(String testMethod) {
+      measure.setTestMethod(testMethod);
+      return this;
+    }
+
+    public MeasureBuilder setExceptionClass(String exceptionClass) {
+      measure.setExceptionClass(exceptionClass);
+      return this;
+    }
+
+    public MeasureBuilder setExceptionMessage(String exceptionMessage) {
+      measure.setExceptionMessage(exceptionMessage);
+      return this;
+    }
+
+    public MeasureBuilder setExceptionLogs(String exceptionLogs) {
+      measure.setExceptionLogs(exceptionLogs);
+      return this;
+    }
+
+    public Measure build() {
+      return measure;
+    }
+  }
+}
diff --git a/test-monitoring/src/main/java/org/sonarqube/monitoring/test/aspect/TestFailureAspect.java b/test-monitoring/src/main/java/org/sonarqube/monitoring/test/aspect/TestFailureAspect.java
new file mode 100644 (file)
index 0000000..2cb05c7
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.monitoring.test.aspect;
+
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.After;
+import org.aspectj.lang.annotation.Aspect;
+import org.junit.runner.Description;
+import org.junit.runner.notification.Failure;
+import org.sonarqube.monitoring.test.Measure;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.sonarqube.monitoring.test.Measure.MeasureBuilder.newMeasureBuilder;
+
+@Aspect
+public class TestFailureAspect {
+
+  public static final String BRANCH_NAME = System.getenv("GITHUB_BRANCH");
+  public static final String COMMIT_HASH = System.getenv("GIT_SHA1");
+  public static final String BUILD_NUMBER = System.getenv("BUILD_NUMBER");
+  public static final String QA_CATEGORY = System.getenv("QA_CATEGORY");
+
+  private static final Path PATH = Paths.get("/tmp/test-monitoring.log");
+  private static final Gson GSON = new Gson();
+
+  static {
+    try {
+      if (!Files.exists(PATH, LinkOption.NOFOLLOW_LINKS)) {
+        Files.createFile(PATH);
+      }
+      Files.write(PATH, "".getBytes(UTF_8));
+    } catch (IOException e) {
+      // Ignore
+    }
+  }
+
+  @After("execution(public * org.junit.runner.notification.RunNotifier+.fireTestFailure(..))")
+  public void afterFireTestFailure(JoinPoint joinPoint) {
+    Object[] args = joinPoint.getArgs();
+    if (args.length == 1) {
+      Object arg = args[0];
+      if (arg instanceof Failure) {
+        Failure failure = (Failure) arg;
+        persistMeasure(buildMeasure(failure));
+      }
+    }
+  }
+
+  private static Measure buildMeasure(Failure failure) {
+    Throwable throwable = failure.getException();
+    Description description = failure.getDescription();
+    return newMeasureBuilder()
+      .setTimestamp(LocalDateTime.now(ZoneId.of("UTC")).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
+      .setBranchName(BRANCH_NAME)
+      .setCommit(COMMIT_HASH)
+      .setBuild(BUILD_NUMBER)
+      .setCategory(QA_CATEGORY)
+      .setTestClass(description.getClassName())
+      .setTestMethod(description.getMethodName())
+      .setExceptionClass(throwable.getClass().getName())
+      .setExceptionMessage(failure.getMessage())
+      .setExceptionLogs(failure.getTrimmedTrace())
+      .build();
+  }
+
+  public static void persistMeasure(Measure measure) {
+    try {
+      Files.write(PATH, GSON.toJson(measure).getBytes(UTF_8), StandardOpenOption.APPEND);
+      Files.write(PATH, "\n".getBytes(UTF_8), StandardOpenOption.APPEND);
+    } catch (IOException e) {
+      // Ignore
+    }
+  }
+}
diff --git a/test-monitoring/src/main/java/org/sonarqube/monitoring/test/aspect/package-info.java b/test-monitoring/src/main/java/org/sonarqube/monitoring/test/aspect/package-info.java
new file mode 100644 (file)
index 0000000..16dd601
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonarqube.monitoring.test.aspect;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/test-monitoring/src/main/java/org/sonarqube/monitoring/test/package-info.java b/test-monitoring/src/main/java/org/sonarqube/monitoring/test/package-info.java
new file mode 100644 (file)
index 0000000..40402c7
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonarqube.monitoring.test;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/test-monitoring/src/main/resources/META-INF/aop.xml b/test-monitoring/src/main/resources/META-INF/aop.xml
new file mode 100644 (file)
index 0000000..32aa78e
--- /dev/null
@@ -0,0 +1,12 @@
+<aspectj>
+    <aspects>
+        <aspect name="org.sonarqube.monitoring.test.aspect.TestFailureAspect"/>
+    </aspects>
+    <weaver options="-warn:none -Xlint:ignore">
+        <include within="com.sonarsource..*"/>
+        <include within="org.sonarqube..*"/>
+        <include within="org.sonar..*"/>
+        <include within="com.sonar..*"/>
+        <include within="org.junit..*"/>
+    </weaver>
+</aspectj>
diff --git a/test-monitoring/src/test/java/org/sonarqube/monitoring/test/aspect/TestFailureAspectTest.java b/test-monitoring/src/test/java/org/sonarqube/monitoring/test/aspect/TestFailureAspectTest.java
new file mode 100644 (file)
index 0000000..1b01be0
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.monitoring.test.aspect;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.aspectj.lang.JoinPoint;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.notification.Failure;
+
+import static java.nio.file.Files.createDirectory;
+import static java.nio.file.Files.exists;
+import static java.nio.file.Files.readAllBytes;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.junit.runner.Description.createTestDescription;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class TestFailureAspectTest {
+
+  private TestFailureAspect testFailureAspect;
+
+  private static final Path TMP_PATH = Path.of("/tmp");
+
+  @BeforeClass
+  public static void createTmpFolder() throws IOException {
+    if (!exists(TMP_PATH)) {
+      createDirectory(TMP_PATH);
+    }
+  }
+
+  @Before
+  public void setup() {
+    testFailureAspect = new TestFailureAspect();
+  }
+
+  @Test
+  public void afterFireTestFailure_shouldPersistMeasure() {
+    JoinPoint joinPoint = mock(JoinPoint.class);
+    Failure failure = new Failure(
+      createTestDescription("testClass", "testMethod"),
+      new IllegalStateException("some exception"));
+    when(joinPoint.getArgs()).thenReturn(new Object[]{failure});
+
+    testFailureAspect.afterFireTestFailure(joinPoint);
+
+    String fileContent = getFileContent(Paths.get("/tmp/test-monitoring.log"));
+    assertThat(fileContent)
+      .contains("\"timestamp\":\"" )
+      .contains("\"testClass\":\"testClass\"")
+      .contains("\"testMethod\":\"testMethod\"")
+      .contains("\"exceptionClass\":\"java.lang.IllegalStateException\"")
+      .contains("\"exceptionMessage\":\"some exception\"")
+      .contains("\"exceptionLogs\":\"java.lang.IllegalStateException: some exception");
+  }
+
+  private String getFileContent(Path path) {
+    try {
+      byte[] bytes = readAllBytes(path);
+      return new String(bytes, StandardCharsets.UTF_8);
+    } catch (IOException e) {
+      fail("Unable to read file " + path, e);
+    }
+    return null;
+  }
+
+}
index db13f826b7bf1ce744ccc898c4f4168cabc04e11..3f4254e63c35bbb1be4e1c9931a3771759577607 100644 (file)
@@ -9,7 +9,7 @@ gradle.aggregateModules=True
 gradle.preferredEnvironment=wrapper
 gradle.innerModulesAsDependencies=True
 gradle.ignoredConfigurations=.*test.* .*bbt.*
-gradle.excludeModules=.*it-.* .*ut-monitoring.* .*sonar-ws-generator.* .*sonar-testing-ldap.* .*sonar-testing-harness.*
+gradle.excludeModules=.*it-.* .*ut-monitoring.* .*test-monitoring.* .*sonar-ws-generator.* .*sonar-testing-ldap.* .*sonar-testing-harness.*
 
 npm.includeDevDependencies=False
 npm.resolveDependencies=True