├── TODO ├── .gitignore ├── .mvn ├── maven.config └── extensions.xml ├── src ├── main │ ├── resources │ │ ├── hudson │ │ │ └── plugins │ │ │ │ └── mstest │ │ │ │ ├── Messages.properties │ │ │ │ ├── Messages_fr_FR.properties │ │ │ │ ├── Messages_it_IT.properties │ │ │ │ ├── Messages_pt_BR.properties │ │ │ │ ├── MSTestPublisher │ │ │ │ ├── config.properties │ │ │ │ ├── config_pt_BR.properties │ │ │ │ ├── config_it_IT.properties │ │ │ │ ├── config_fr_FR.properties │ │ │ │ └── config.jelly │ │ │ │ ├── codecoverage2vstest.console.xsl │ │ │ │ ├── vstest.console2codecoverage.xsl │ │ │ │ └── mstest-to-junit.xsl │ │ └── index.jelly │ ├── java │ │ └── hudson │ │ │ └── plugins │ │ │ └── mstest │ │ │ ├── FileOperator.java │ │ │ ├── XslTransformer.java │ │ │ ├── MsTestLogger.java │ │ │ ├── FileResolver.java │ │ │ ├── ContentCorrector.java │ │ │ ├── MSTestTransformer.java │ │ │ ├── MSTestReportConverter.java │ │ │ └── MSTestPublisher.java │ └── webapp │ │ ├── help_pt_BR.html │ │ ├── help_it_IT.html │ │ ├── help_fr_FR.html │ │ └── help.html └── test │ ├── resources │ └── hudson │ │ └── plugins │ │ └── mstest │ │ ├── webTestResult.xml │ │ ├── JENKINS-13862.xml │ │ ├── JENKINS-17506.xml │ │ ├── ReportingOptions.2008.xml │ │ ├── junit_mstest_2_tests_from_different_assemblies.xml │ │ ├── ReportingOptions.2013.xml │ │ ├── junit_mstest_2_tests_1_class.xml │ │ ├── mstest_vs_2010.xml │ │ ├── mstest_vs_2012.xml │ │ ├── junit_mstest_more_than-one_minute_test.xml │ │ ├── kozl-unit-tests-missing.xml │ │ ├── junit_mstest_4_tests_2_classes.xml │ │ ├── DataDriven.2008.xml │ │ ├── DataDriven.2013.xml │ │ ├── webTestResult-decoded.xml │ │ ├── JENKINS-25533+19384.xml │ │ ├── nilleb_HOST18468 2015-03-18 08_03_36.xml │ │ ├── JENKINS-13862.trx │ │ ├── JENKINS-17506.trx │ │ ├── mstest_vs_2012.trx │ │ ├── mstest_vs_2012_timeout.trx │ │ ├── mstest_2_tests_from_different_assemblies.trx │ │ ├── mstest_2_tests_1_class.trx │ │ ├── mstest_more_than_one_minute_test.trx │ │ ├── mstest_vs_2010.trx │ │ ├── JENKINS-23531-xmlentities-forged.trx │ │ ├── JENKINS-23531-charset.trx │ │ ├── DataDriven-no-tests-found.trx │ │ ├── ReportingOptions.2008.trx │ │ ├── ReportingOptions.2013.trx │ │ ├── JENKINS-25533+19384.trx │ │ ├── kozl-unit-tests-missing.trx │ │ ├── webTestResult.trx │ │ └── mstest_4_tests_2_classes.trx │ └── java │ └── hudson │ └── plugins │ └── mstest │ ├── TestHelper.java │ ├── MsTestLoggerTest.java │ ├── FileResolverTest.java │ ├── MSTestTransformerTest.java │ ├── MSTestTransformerAndConverterTest.java │ ├── MSTestPublisherJenkinsRuleTest.java │ └── MSTestPublisherTest.java ├── README.md ├── Jenkinsfile ├── .github └── workflows │ └── jenkins-security-scan.yml ├── LICENSE └── pom.xml /TODO: -------------------------------------------------------------------------------- 1 | delete files generated by content corrector 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /work/ 3 | .DS_Store 4 | .idea 5 | *.iml -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/Messages.properties: -------------------------------------------------------------------------------- 1 | MsTest.Publisher.Name=Publish MSTest test result report -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/Messages_fr_FR.properties: -------------------------------------------------------------------------------- 1 | MsTest.Publisher.Name=Publier un rapport de test MSTest -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/Messages_it_IT.properties: -------------------------------------------------------------------------------- 1 | MsTest.Publisher.Name=Pubblica un rapporto di test MSTest -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/Messages_pt_BR.properties: -------------------------------------------------------------------------------- 1 | MsTest.Publisher.Name=Publicar relat\u00F3rio de testes do MSTest -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/MSTestPublisher/config.properties: -------------------------------------------------------------------------------- 1 | description.pattern=A path relative to the workspace root, an Ant fileset pattern, or an environment variable. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mstest-plugin 2 | ============= 3 | 4 | Jenkins MSTest plugin 5 | 6 | [![Build Status](https://ci.jenkins.io/buildStatus/icon?job=Plugins/mstest-plugin/master)](https://ci.jenkins.io/job/Plugins/mstest-plugin/master) 7 | 8 | [SonarCloud report](https://sonarcloud.io/dashboard/index/org.jvnet.hudson.plugins:mstest) -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | This plugin converts MSTest 5 | TRX test reports into JUnit XML reports so it can be integrated with Hudson's JUnit features. 6 |
7 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/MSTestPublisher/config_pt_BR.properties: -------------------------------------------------------------------------------- 1 | FIXME=a translator is required. 2 | Test\ report\ TRX\ file=Arquivo TRX com resultado dos testes 3 | description.pattern=O caminho para o arquivo come\u00e7a na raiz do workspace. 4 | Retain\ long\ standard\ output/error=Manter padr\u00e3o de erro de sa\u00edda 5 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.7 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/webTestResult.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/MSTestPublisher/config_it_IT.properties: -------------------------------------------------------------------------------- 1 | Test\ report\ TRX\ file=Percorso del rapporto al formato TRX 2 | description.pattern=Il percorso relativo alla radice del workspace, un pattern ant per un fileset o una variable d''ambiente. 3 | Fail\ build\ if\ no\ files\ are\ found=Generare un errore se nessun file TRX viene trovato 4 | Retain\ long\ standard\ output/error=Mostrare la totalit\u00e0 dei messaggi inviati sull'uscita/errore standard durante l''esecuzione dei test -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/MSTestPublisher/config_fr_FR.properties: -------------------------------------------------------------------------------- 1 | Test\ report\ TRX\ file=Chemin du rapport au format TRX 2 | description.pattern=Chemin relatif \u00e0 la racine du workspace, un patron Ant pour un fileset ou une variable d''environnement. 3 | Fail\ build\ if\ no\ files\ are\ found=Faire \u00E9chouer la construction si aucun fichier TRX est trouv\u00E9 4 | Retain\ long\ standard\ output/error=Afficher l''enti\u00E8ret\u00E9 des messages affich\u00E9s sur la sortie standard/erreur standard -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* 2 | See the documentation for more options: 3 | https://github.com/jenkins-infra/pipeline-library/ 4 | */ 5 | buildPlugin( 6 | forkCount: '1C', // run this number of tests in parallel for faster feedback. If the number terminates with a 'C', the value will be multiplied by the number of available CPU cores 7 | useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests 8 | configurations: [ 9 | [platform: 'linux', jdk: 21], 10 | [platform: 'windows', jdk: 17], 11 | ]) 12 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/JENKINS-13862.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/JENKINS-17506.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | given RomanCalc object exists 5 | when args I and II are passed to the Add Method 6 | then III is returned 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/ReportingOptions.2008.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test-alternate-name:Use This Name Instead: 6 | 7 | 8 | 9 | test-instance-name:My Suffix: 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/junit_mstest_2_tests_from_different_assemblies.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | at GoodGuyTest.GoodGuyTest.TestYell() in C:\PostIt\MultipleTests\GoodGuyTest\GoodGuyTest.cs:line 16 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/ReportingOptions.2013.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test-alternate-name:Use This Name Instead: 6 | 7 | 8 | 9 | test-instance-name:My Suffix: 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/junit_mstest_2_tests_1_class.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | at FunctionsTest.FibonacciTest.CalculaFibonacci_2() in E:\Koiti\Dev\GitHudsonTest\FunctionsTest\FibonacciTest.cs:line 72 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/mstest_vs_2010.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | at TestProject1.UnitTest1.TestMethod2() in c:\users\carlos\documents\visual studio 2010\Projects\TestProject1\TestProject1\UnitTest1.cs:line 71 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/mstest_vs_2012.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | at TestProject1.UnitTest1.TestMethod2() in c:\users\carlos\documents\visual studio 2010\Projects\TestProject1\TestProject1\UnitTest1.cs:line 71 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/junit_mstest_more_than-one_minute_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | at FunctionsTest.FibonacciTest.CalculaFibonacci_2() in E:\Koiti\Dev\GitHudsonTest\FunctionsTest\FibonacciTest.cs:line 72 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/MSTestPublisher/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2017, contributors of the MSTest plugin project 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/mstest/TestHelper.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import hudson.FilePath; 4 | import hudson.Util; 5 | import java.io.File; 6 | import org.jmock.Mockery; 7 | import org.jmock.lib.legacy.ClassImposteriser; 8 | 9 | public abstract class TestHelper { 10 | 11 | protected FilePath workspace; 12 | File parentFile; 13 | 14 | void createWorkspace() throws Exception { 15 | parentFile = Util.createTempDir(); 16 | workspace = new FilePath(parentFile); 17 | if (workspace.exists()) { 18 | workspace.deleteRecursive(); 19 | } 20 | workspace.mkdirs(); 21 | } 22 | 23 | void deleteWorkspace() throws Exception { 24 | workspace.deleteRecursive(); 25 | } 26 | 27 | 28 | Mockery getClassMock() { 29 | Mockery classContext; 30 | classContext = new Mockery() { 31 | { 32 | setImposteriser(ClassImposteriser.INSTANCE); 33 | } 34 | }; 35 | return classContext; 36 | } 37 | 38 | Mockery getMock() { 39 | return new Mockery(); 40 | } 41 | 42 | String[] resolve(String testFile) { 43 | return new FileResolver(null).FindMatchingMSTestReports(testFile, workspace); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/kozl-unit-tests-missing.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/junit_mstest_4_tests_2_classes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | at FunctionsTest.FibonacciTest.CalculaFibonacci_2() in E:\Koiti\Dev\GitHudsonTest\FunctionsTest\FibonacciTest.cs:line 72 6 | 7 | 8 | 9 | 10 | at FunctionsTest.SpecialCharRemoverTest.REmoveSpecialChar2() in E:\Koiti\Dev\GitHudsonTest\FunctionsTest\SpecialCharRemoverTest.cs:line 76 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/mstest/FileOperator.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import java.io.File; 4 | 5 | class FileOperator { 6 | 7 | static boolean safeDelete(File path, MsTestLogger logger) { 8 | boolean success = path.delete(); 9 | if (!success) { 10 | logger.warn("Unable to delete the file %s", path.getAbsolutePath()); 11 | logger.info("This file is a reserved temporary file. You can delete it safely."); 12 | } 13 | return success; 14 | } 15 | 16 | static boolean safeCreateFolder(File path, MsTestLogger logger) { 17 | boolean success = true; 18 | if (path.isFile()) { 19 | success = path.delete(); 20 | if (!success) { 21 | logger.error("Unable to delete the file: %s.", path.getAbsolutePath()); 22 | } 23 | } 24 | if (success && !path.exists()) { 25 | success = path.mkdirs(); 26 | if (!success) { 27 | logger.error("Unable to create the folder: %s", path.getAbsolutePath()); 28 | } 29 | } 30 | if (!success) { 31 | logger.info( 32 | "The path %s is expected to be a folder, writable by the Jenkins worker process.", 33 | path.getAbsolutePath()); 34 | } 35 | return success; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/mstest/MsTestLoggerTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import java.util.logging.Level; 4 | import org.junit.Test; 5 | import hudson.plugins.mstest.MsTestLogger; 6 | 7 | import static org.junit.Assert.assertTrue; 8 | 9 | public class MsTestLoggerTest { 10 | 11 | @Test 12 | public void testConstructor() { 13 | // arrange 14 | System.clearProperty(MsTestLogger.HUDSON_PLUGINS_MSTEST_LEVEL); 15 | 16 | // act 17 | MsTestLogger logger = new MsTestLogger(null); 18 | 19 | // assert 20 | Level logLevel = logger.getConfiguredLogLevel(); 21 | assertTrue(logLevel == Level.INFO); 22 | } 23 | 24 | @Test(expected = RuntimeException.class) 25 | public void testConstructorShouldThrowExceptionWhenUnknownLevel() { 26 | // arrange 27 | System.setProperty(MsTestLogger.HUDSON_PLUGINS_MSTEST_LEVEL, "INVALID_LEVEL"); 28 | 29 | // act 30 | MsTestLogger logger = new MsTestLogger(null); 31 | } 32 | 33 | @Test 34 | public void testConstructorShouldParseLogLevel() { 35 | // arrange 36 | System.setProperty(MsTestLogger.HUDSON_PLUGINS_MSTEST_LEVEL, "WARNING"); 37 | 38 | // act 39 | MsTestLogger logger = new MsTestLogger(null); 40 | 41 | // assert 42 | Level logLevel = logger.getConfiguredLogLevel(); 43 | assertTrue(logLevel == Level.WARNING); 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/webapp/help_pt_BR.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | FIXME not in sync with the english version. Needs the help of some portuguese/brazilian translator. 4 |
5 | O plugin do MSTest converte relatórios de teste em formato TRX em relatórios XML do JUnit para que possam ser interpretados pelo Hudson. 6 |
7 | 8 | Para habilitar as funcionalidades do plugin você precisa configurar seu build para primeiramente executar o MSTest. Exemplo:

9 | "%PROGRAMFILES%\Microsoft Visual Studio 9.0\Common7\IDE\MSTest.exe" /runconfig:MyTestProject\LocalTestRun.testrunconfig /testcontainer:MyTestProject\bin\Release\MyTestProject.dll /resultsfile:TestResults\testResults.trx

10 | e então especificar o caminho para o arquivo TRX gerado pelo MSTest, como: TestResults/testResults.trx.

11 | Você pode também executar multiplas assemblies de teste. A configuração é parecida, basta adicionar outro testcontainer:

12 | "%PROGRAMFILES%\Microsoft Visual Studio 9.0\Common7\IDE\MSTest.exe" /runconfig:MyTestProject\LocalTestRun.testrunconfig /testcontainer:MyTestProject1\bin\Release\MyTestProject1.dll /testcontainer:MyTestProject2\bin\Release\MyTestProject2.dll /resultsfile:TestResults\testResults.trx

13 | O MSTest irá executar todos os testes de todas as assemblies e será gerado o arquivo TRX especificado. 14 |

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/DataDriven.2008.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | test-alternate-name:Case C: 8 | 9 | 10 | 11 | test-alternate-name:Case A: 12 | 13 | 14 | 15 | test-alternate-name:Case B: 16 | 17 | 18 | 19 | test-instance-name:Case B: 20 | 21 | 22 | 23 | test-instance-name:Case C: 24 | 25 | 26 | 27 | test-instance-name:Case A: 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/DataDriven.2013.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | test-alternate-name:Case B: 8 | 9 | 10 | 11 | test-alternate-name:Case C: 12 | 13 | 14 | 15 | test-alternate-name:Case A: 16 | 17 | 18 | 19 | test-instance-name:Case C: 20 | 21 | 22 | 23 | test-instance-name:Case A: 24 | 25 | 26 | 27 | test-instance-name:Case B: 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/webapp/help_it_IT.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Il plugin MSTest può convertire i rapporti di test al format TRX in rapporti di test nel formato XML di JUnit. 4 | In questo modo, Hudson/Jenkins potrà mostrarli sul rapporto di build. 5 | 6 |
7 | Innanzitutto, pensate a produrre un rapporto di test al formato TRX. Rapporti in questo formato possono essere prodotti da MSTest o da VSTest.Console. 8 | Per esempio, il comando seguente: 9 |
10 |
11 | "%PROGRAMFILES%\Microsoft Visual Studio 9.0\Common7\IDE\MSTest.exe" /runconfig:MyTestProject\LocalTestRun.testrunconfig /testcontainer:MyTestProject\bin\Release\MyTestProject.dll /resultsfile:TestResults\testResults.trx 12 |

13 | Produce un rapporto in 'TestResults/testResults.trx'. Specificate dunque questo valore come percorso. 14 |

15 | VSTest.console non permette più di specificare un percorso completo per il rapporto di test. Per risolvere questa limitazione, potete specificare un pattern come 'TestResults/**/*.trx'. (/ o \, non fa alcuna differenza). 16 |

17 | Qualora utilizzaste vstestrunner-plugin, il percorso verso il file TRX sarebbe disponibile in una variable d'ambiente. In questo caso, potreste specificare $VSTEST_RESULT_TRX o ${VSTEST_RESULT_TRX}. 18 |

19 | In tutti gli esempi qui sopra, il carattere ' è stato utilizzato come delimitatore dei pattern da utilizzare. Questi caratteri non dovranno far parte del vostro pattern. 20 |

21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/webapp/help_fr_FR.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Le m-plugin MSTest peut convertir les rapports de test au format TRX en rapport de testau format XML produit par JUnit. 4 | De cette façon, Hudson/Jenkins pourront les afficher sur le rapport de build. 5 | 6 |
7 | D'abord, commencez à produire un rapport de test au format TRX. Des rapports en ce format pevent être produits par MSTest ou par VSTest.Console. 8 | Pour exemple, la commande suivante: 9 |
10 |
11 | "%PROGRAMFILES%\Microsoft Visual Studio 9.0\Common7\IDE\MSTest.exe" /runconfig:MyTestProject\LocalTestRun.testrunconfig /testcontainer:MyTestProject\bin\Release\MyTestProject.dll /resultsfile:TestResults\testResults.trx 12 |

13 | Produit un rapport en 'TestResults/testResults.trx'. Utiisez cette valeur en tant que chemin. 14 |

15 | VSTest.Console ne permet plus de spécifier un chemin complet pour le rapport de test. Pour contourner ce problme, vous pouvez spécifier un patron Ant pour un fileset, comme 'TestResults/**/*.trx' (/ ou \ peuvent être utilisés sans importance). 16 |

17 | Si vous utilisiez le vstestrunner-plugin, le chemin vers le fichier TRX serait disponible en tant que variable d'environnement. Vous pourriez donc utiliser une expression comme $VSTEST_RESULT_TRX ou ${VSTEST_RESULT_TRX}. 18 |

19 | Dans les exemples ci-dessus, le caractère ' a été utilisé en tant que délimitateur des patrons. Il ne devra pas faire partie des patrons que vous allez spécifier. 20 |

21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/webTestResult-decoded.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/JENKINS-25533+19384.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | at ArtOfTest.Common.UnitTesting.Assert.AreEqual[T](T expected, T actual, String message) 5 | at ArtOfTest.Common.UnitTesting.Assert.AreEqual[T](T expected, T actual) 6 | at UITestAutomation.SimpleChecks.IsLoginDialogShown() in d:\tests_Workspace\src\TestAutomation\TestActions.cs:line 455 7 | at UITestAutomation.SimpleChecks.CheckLoginDialogShown(Boolean exp, String msg) in d:\tests_Workspace\src\TestAutomation\TestActions.cs:line 425 8 | at UITestAutomation.SimpleActions.LoginAs(String user, String password) in d:\tests_Workspace\src\TestAutomation\TestActions.cs:line 76 9 | at SanityCheck.Login() in d:\tests_Workspace\src\TestAutomation\SanityCheck.cs:line 25 10 | 11 | 17:43:23 ..Failed to connect: Attempt 0 12 | 17:43:25 ..Connected to CLIENT : v1.0.511.0 13 | 17:43:25 [PASS] Check Login dialog is presented on the main page 14 | 17:43:25 [PASS] Check OK button is disabled on initial Login Screen 15 | 17:43:27 ..Entered user name 16 | 17:43:28 ..Enter user password 17 | 17:43:28 [PASS] Check OK button became enabled after entered credentials 18 | 17:43:29 ..Confirmed login by OK button press 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/mstest/FileResolverTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import hudson.FilePath; 4 | import hudson.model.TaskListener; 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.File; 7 | import java.io.FileOutputStream; 8 | import java.io.InputStream; 9 | import java.io.PrintStream; 10 | import org.jmock.Expectations; 11 | import org.jmock.Mockery; 12 | import org.junit.After; 13 | import org.junit.Assert; 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | import org.springframework.util.FileCopyUtils; 17 | 18 | public class FileResolverTest extends TestHelper { 19 | 20 | private TaskListener buildListener; 21 | private Mockery classContext; 22 | 23 | @Before 24 | public void setUp() throws Exception { 25 | createWorkspace(); 26 | 27 | classContext = getClassMock(); 28 | buildListener = classContext.mock(TaskListener.class); 29 | } 30 | 31 | @After 32 | public void tearDown() throws Exception { 33 | deleteWorkspace(); 34 | } 35 | 36 | 37 | @Test 38 | public void testWorkspaceList() throws Exception { 39 | final PrintStream logger = new PrintStream(new ByteArrayOutputStream()); 40 | classContext.checking(new Expectations() { 41 | { 42 | ignoring(buildListener).getLogger(); 43 | will(returnValue(logger)); 44 | ignoring(buildListener); 45 | } 46 | }); 47 | 48 | File subfolder = new File(parentFile, "subfolder"); 49 | assert subfolder.exists() || subfolder.mkdirs(); 50 | File testFile = new File(subfolder, "xmlentities-forged.trx"); 51 | assert !testFile.exists() || testFile.delete(); 52 | InputStream testStream = this.getClass() 53 | .getResourceAsStream("JENKINS-23531-xmlentities-forged.trx"); 54 | FileCopyUtils.copy(testStream, new FileOutputStream(testFile)); 55 | FilePath[] list = workspace.list("*.trx"); 56 | Assert.assertEquals(0, list.length); 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/mstest/MSTestTransformerTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import hudson.AbortException; 4 | import hudson.model.TaskListener; 5 | import hudson.remoting.VirtualChannel; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.PrintStream; 8 | import org.jmock.Expectations; 9 | import org.jmock.Mockery; 10 | import org.junit.After; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | 15 | /** 16 | * Unit tests for MSTestTransformer class 17 | * 18 | * @author Antonio Marques 19 | */ 20 | public class MSTestTransformerTest extends TestHelper { 21 | 22 | 23 | private TaskListener buildListener; 24 | private Mockery context; 25 | private Mockery classContext; 26 | private MSTestReportConverter converter; 27 | private MSTestTransformer transformer; 28 | private VirtualChannel virtualChannel; 29 | 30 | 31 | @Before 32 | public void setUp() throws Exception { 33 | createWorkspace(); 34 | 35 | context = getMock(); 36 | classContext = getClassMock(); 37 | 38 | buildListener = classContext.mock(TaskListener.class); 39 | converter = classContext.mock(MSTestReportConverter.class); 40 | virtualChannel = context.mock(VirtualChannel.class); 41 | } 42 | 43 | @After 44 | public void tearDown() throws Exception { 45 | deleteWorkspace(); 46 | } 47 | 48 | @Test 49 | public void testReturnWhenNoTRXFileIsFound() throws Exception { 50 | classContext.checking(new Expectations() { 51 | { 52 | ignoring(buildListener).getLogger(); 53 | will(returnValue(new PrintStream(new ByteArrayOutputStream()))); 54 | oneOf(buildListener).fatalError(with(any(String.class))); 55 | } 56 | }); 57 | 58 | transformer = new MSTestTransformer(resolve("build.trx"), converter, buildListener, true); 59 | try { 60 | transformer.invoke(parentFile, virtualChannel); 61 | Assert.fail("The archiver did not throw when it could not find any files"); 62 | } catch (AbortException ae) { 63 | Assert.assertTrue(true); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/mstest/XslTransformer.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import javax.xml.XMLConstants; 8 | import javax.xml.transform.Transformer; 9 | import javax.xml.transform.TransformerConfigurationException; 10 | import javax.xml.transform.TransformerException; 11 | import javax.xml.transform.TransformerFactory; 12 | import javax.xml.transform.dom.DOMSource; 13 | import javax.xml.transform.stream.StreamResult; 14 | import javax.xml.transform.stream.StreamSource; 15 | 16 | /** 17 | * @author nilleb 18 | */ 19 | class XslTransformer { 20 | 21 | private final transient Transformer xslTransformer; 22 | 23 | XslTransformer() 24 | throws TransformerConfigurationException { 25 | TransformerFactory transformerFactory = TransformerFactory.newInstance(); 26 | transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 27 | transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); 28 | xslTransformer = transformerFactory.newTransformer(); 29 | } 30 | 31 | private XslTransformer(String xslTransform) 32 | throws TransformerConfigurationException { 33 | TransformerFactory transformerFactory = TransformerFactory.newInstance(); 34 | transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 35 | transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); 36 | xslTransformer = transformerFactory 37 | .newTransformer(new StreamSource(this.getClass().getResourceAsStream(xslTransform))); 38 | } 39 | 40 | static XslTransformer FromResource(String resourceName) 41 | throws TransformerConfigurationException { 42 | return new XslTransformer(resourceName); 43 | } 44 | 45 | void transform(InputStream inputStream, File outputFile) 46 | throws TransformerException, IOException { 47 | try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile)) { 48 | xslTransformer 49 | .transform(new StreamSource(inputStream), new StreamResult(fileOutputStream)); 50 | } 51 | } 52 | 53 | void transform(DOMSource source, File outputFile) 54 | throws TransformerException, IOException { 55 | try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile)) { 56 | xslTransformer.transform(source, new StreamResult(fileOutputStream)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/nilleb_HOST18468 2015-03-18 08_03_36.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | à green.Tests.PipeLineTest.failing() dans k:\GitRepositories\green\green.Tests\PipeLineTest.cs:ligne 30 9 | 10 | 11 | 12 | 13 | 14 | 15 | depget: installing depget.0.5.1 16 | * the package 'depget version 0.5.1' has been downloaded. 17 | - copying the folder 'content' to cwd 18 | depget: installing Cegid.MsiMake.8.0.6 19 | * the package 'Cegid.MsiMake version 8.0.6' has been downloaded. 20 | - copying the folder 'content' to cwd 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/mstest/MsTestLogger.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import hudson.model.TaskListener; 4 | import java.io.Serializable; 5 | import java.util.logging.Level; 6 | import java.util.logging.Logger; 7 | 8 | class MsTestLogger implements Serializable { 9 | 10 | private static String prefix = "[MSTEST-PLUGIN]"; 11 | private TaskListener listener; 12 | private Level configuredLevel; 13 | public static String HUDSON_PLUGINS_MSTEST_LEVEL = "hudson.plugins.mstest.level"; 14 | private static String ERROR_LEVEL = "ERROR"; 15 | private static String WARNING_LEVEL = "WARNING"; 16 | private static String DEBUG_LEVEL = "DEBUG"; 17 | private static String INFO_LEVEL = "INFO"; 18 | 19 | private static final Logger logger = Logger.getLogger(MSTestReportConverter.class.getName()); 20 | 21 | MsTestLogger(TaskListener listener) { 22 | this.listener = listener; 23 | String mstestLevel = System.getProperty(HUDSON_PLUGINS_MSTEST_LEVEL); 24 | this.configuredLevel = parseLevel(mstestLevel != null ? mstestLevel : MSTestPublisher.DescriptorImpl.defaultLogLevel); 25 | } 26 | 27 | static MsTestLogger getLogger() { 28 | return new MsTestLogger(null); 29 | } 30 | 31 | static String format(String message) { 32 | return String.format("%s %s", MsTestLogger.prefix, message); 33 | } 34 | 35 | void debug(String format, Object... args) { 36 | printf(Level.FINE, format, args); 37 | } 38 | 39 | void info(String format, Object... args) { 40 | printf(Level.INFO, format, args); 41 | } 42 | 43 | void warn(String format, Object... args) { 44 | printf(Level.WARNING, format, args); 45 | } 46 | 47 | void error(String format, Object... args) { 48 | printf(Level.SEVERE, format, args); 49 | } 50 | 51 | Level getConfiguredLogLevel() { 52 | return configuredLevel; 53 | } 54 | 55 | private void printf(Level level, String format, Object... args) { 56 | String messageFormat = String 57 | .format("%s %s %s%n", MsTestLogger.prefix, level.getName(), format); 58 | if (listener != null) { 59 | if(shouldLog(level)){ 60 | listener.getLogger().printf(messageFormat, args); 61 | } 62 | } else { 63 | logger.setLevel(this.configuredLevel); 64 | logger.log(level, messageFormat, args); 65 | } 66 | } 67 | 68 | private boolean shouldLog(Level level){ 69 | if(this.configuredLevel != null){ 70 | return level.intValue() >= configuredLevel.intValue(); 71 | } else { 72 | return true; 73 | } 74 | } 75 | 76 | public static Level parseLevel(String level) throws RuntimeException { 77 | if(ERROR_LEVEL.equals(level)){ 78 | return Level.SEVERE; 79 | } else if (WARNING_LEVEL.equals(level)){ 80 | return Level.WARNING; 81 | } else if (DEBUG_LEVEL.equals(level)){ 82 | return Level.FINE; 83 | } else if (INFO_LEVEL.equals(level)){ 84 | return Level.INFO; 85 | } else { 86 | throw new RuntimeException(String.format("Unknown [%s] level provided!", level)); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/mstest/FileResolver.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import hudson.EnvVars; 4 | import hudson.FilePath; 5 | import hudson.model.Run; 6 | import hudson.model.TaskListener; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | 11 | class FileResolver { 12 | 13 | private final TaskListener listener; 14 | 15 | FileResolver(TaskListener listener) { 16 | this.listener = listener; 17 | } 18 | 19 | String SafeResolveFilePath(String filePath, Run build, TaskListener listener) { 20 | MsTestLogger logger = new MsTestLogger(listener); 21 | String resolved = filePath; 22 | EnvVars env = null; 23 | try { 24 | env = build.getEnvironment(listener); 25 | } catch (IOException ioe) { 26 | logger 27 | .error("caught unexpected IO exception while retrieving environment variables: %s", 28 | ioe.getMessage()); 29 | } catch (InterruptedException ie) { 30 | logger.error( 31 | "caught unexpected interrupted exception while retrieving environment variables: %s", 32 | ie.getMessage()); 33 | Thread.currentThread().interrupt(); 34 | } 35 | if (env != null) { 36 | resolved = resolveFilePath(filePath, env); 37 | } 38 | return resolved; 39 | } 40 | 41 | private String resolveFilePath(String filePath, EnvVars env) { 42 | MsTestLogger logger = new MsTestLogger(listener); 43 | String resolvedFilePath = filePath; 44 | String expanded = env.expand(resolvedFilePath); 45 | if (expanded != null) { 46 | resolvedFilePath = expanded; 47 | } 48 | if (!filePath.equals(resolvedFilePath)) { 49 | logger.debug("the path %s has been resolved to %s", filePath, resolvedFilePath); 50 | } 51 | logger.info("processing test results in file(s) %s", resolvedFilePath); 52 | return resolvedFilePath; 53 | } 54 | 55 | /** 56 | * Returns all MSTest report files matching the pattern given in configuration 57 | * 58 | * @param pattern the pattern the files shall match 59 | * @param workspace the build's workspace 60 | * @return an array of strings containing filenames of MSTest report files 61 | */ 62 | String[] FindMatchingMSTestReports(String pattern, FilePath workspace) { 63 | MsTestLogger logger = new MsTestLogger(listener); 64 | if (workspace == null) { 65 | return new String[]{}; 66 | } 67 | File f = new File(pattern); 68 | if (f.isAbsolute() && f.exists()) { 69 | return new String[]{f.getAbsolutePath()}; 70 | } 71 | ArrayList fileNames = new ArrayList<>(); 72 | try { 73 | for (FilePath x : workspace.list(pattern)) { 74 | fileNames.add(x.getRemote()); 75 | } 76 | } catch (IOException ioe) { 77 | logger.error("while listing workspace files: %s", ioe.getMessage()); 78 | } catch (InterruptedException ie) { 79 | logger.error("while listing workspace files: %s", ie.getMessage()); 80 | Thread.currentThread().interrupt(); 81 | } 82 | return fileNames.toArray(new String[0]); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/mstest/ContentCorrector.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import com.google.common.base.Charsets; 4 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 5 | import java.io.BufferedReader; 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.io.OutputStreamWriter; 12 | import java.io.PrintWriter; 13 | import java.util.Random; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | 17 | /** 18 | * @author nilleb 19 | */ 20 | class ContentCorrector { 21 | 22 | private static final String ILLEGAL_ENTITIES_PATTERN = "(&#x([0-9A-Fa-f]{1,4});)"; 23 | 24 | private String file; 25 | 26 | ContentCorrector(String file) { 27 | this.file = file; 28 | } 29 | 30 | @SuppressFBWarnings("DMI_RANDOM_USED_ONLY_ONCE") 31 | private static int randInt(int min, int max) { 32 | return new Random().nextInt((max - min) + 1) + min; 33 | } 34 | 35 | void fix() throws IOException { 36 | String filename = randInt(1000, 1000000) + ".trx"; 37 | File inFile = new File(file); 38 | File parent = inFile.getParentFile(); 39 | File outfile = new File(parent, filename); 40 | boolean replace = false; 41 | 42 | try (PrintWriter out = new PrintWriter( 43 | new OutputStreamWriter(new FileOutputStream(outfile), Charsets.UTF_8))) { 44 | try (BufferedReader in = new BufferedReader( 45 | new InputStreamReader(new FileInputStream(inFile), Charsets.UTF_8))) { 46 | String line = in.readLine(); 47 | while (line != null) { 48 | String newline = stripIllegalEntities(line); 49 | if (line.length() != newline.length()) { 50 | replace = true; 51 | } 52 | out.println(newline); 53 | line = in.readLine(); 54 | } 55 | } 56 | } 57 | MsTestLogger logger = new MsTestLogger(null); 58 | 59 | if (replace) { 60 | FileOperator.safeDelete(inFile, logger); 61 | boolean success = outfile.renameTo(inFile); 62 | if (!success) { 63 | logger.error("Unable to move the file %s to %s.", outfile.getAbsolutePath(), 64 | inFile.getAbsolutePath()); 65 | } 66 | } else { 67 | FileOperator.safeDelete(outfile, logger); 68 | } 69 | } 70 | 71 | private String stripIllegalEntities(String line) { 72 | Pattern p = Pattern.compile(ILLEGAL_ENTITIES_PATTERN); 73 | Matcher m = p.matcher(line); 74 | String replaced = line; 75 | 76 | while (m.find()) { 77 | String charGroup = m.group(2); 78 | long c = Long.parseLong(charGroup, 16); 79 | if (!isAllowed(c)) { 80 | replaced = new StringBuilder(replaced).replace(m.start(1), m.end(1), "").toString(); 81 | m = p.matcher(replaced); 82 | } 83 | } 84 | return replaced; 85 | } 86 | 87 | private boolean isAllowed(long c) { 88 | return c == 9 || c == 0xA || c == 0xD || (c > 0x20 && c < 0xD7FF) || (c > 0xE000 89 | && c < 0xFFFD) || (c > 0x10000 && c < 0x10FFFF); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/JENKINS-13862.trx: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | This is a default test run configuration for a local test run. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 45 | 46 | 47 | The agent process was stopped while the test was running. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/codecoverage2vstest.console.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | () 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 0 44 | 1 45 | 2 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/JENKINS-17506.trx: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | This is a default test run configuration for a local test run. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 45 | 46 | 47 | given RomanCalc object exists 48 | when args I and II are passed to the Add Method 49 | then III is returned 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/mstest/MSTestTransformer.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.AbortException; 5 | import hudson.FilePath; 6 | import hudson.model.TaskListener; 7 | import hudson.remoting.Channel; 8 | import hudson.remoting.VirtualChannel; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import javax.xml.parsers.ParserConfigurationException; 12 | import javax.xml.transform.TransformerException; 13 | import jenkins.MasterToSlaveFileCallable; 14 | import org.xml.sax.SAXException; 15 | 16 | /** 17 | * Class responsible for transforming the MSTest build report into a JUnit file and then record it 18 | * in the JUnit result archive. 19 | * 20 | * @author Antonio Marques 21 | */ 22 | public class MSTestTransformer extends MasterToSlaveFileCallable { 23 | 24 | static final String JUNIT_REPORTS_PATH = "temporary-junit-reports"; 25 | private static final long serialVersionUID = 1L; 26 | private final TaskListener listener; 27 | private final boolean failOnError; 28 | 29 | private final MSTestReportConverter unitReportTransformer; 30 | private final String[] msTestFiles; 31 | 32 | MSTestTransformer(String[] msTestFiles, @NonNull MSTestReportConverter unitReportTransformer, 33 | @NonNull TaskListener listener, boolean failOnError) { 34 | this.msTestFiles = msTestFiles; 35 | this.unitReportTransformer = unitReportTransformer; 36 | this.listener = listener; 37 | this.failOnError = failOnError; 38 | } 39 | 40 | /** 41 | * Performs the computational task on the node where the data is located. 42 | * 43 | *

44 | * All the exceptions are forwarded to the caller. 45 | * 46 | * @param ws {@link File} that represents the local file that {@link FilePath} has represented. 47 | * @param channel The "back pointer" of the {@link Channel} that represents the communication 48 | * with the node from where the code was sent. 49 | */ 50 | public Boolean invoke(File ws, VirtualChannel channel) throws IOException { 51 | MsTestLogger logger = new MsTestLogger(listener); 52 | 53 | if (msTestFiles.length == 0) { 54 | if (!failOnError) { 55 | logger.warn("No MSTest TRX test report files were found. Ignoring."); 56 | return Boolean.TRUE; 57 | } 58 | throw new AbortException(MsTestLogger 59 | .format("No MSTest TRX test report files were found. Configuration error?")); 60 | } 61 | 62 | File junitOutputPath = new File(ws, JUNIT_REPORTS_PATH); 63 | boolean success = FileOperator.safeCreateFolder(junitOutputPath, logger); 64 | if (!success) { 65 | return Boolean.FALSE; 66 | } 67 | 68 | for (String mstestFile : msTestFiles) { 69 | logger.info("processing report file: " + mstestFile); 70 | try { 71 | new ContentCorrector(mstestFile).fix(); 72 | unitReportTransformer.transform(mstestFile, junitOutputPath); 73 | } catch (TransformerException | SAXException te) { 74 | throw new IOException( 75 | MsTestLogger.format( 76 | "Unable to transform the MSTest report. Please report this issue to the plugin author"), 77 | te); 78 | } catch (ParserConfigurationException pce) { 79 | throw new IOException( 80 | MsTestLogger.format( 81 | "Unable to initalize the XML parser. Please report this issue to the plugin author"), 82 | pce); 83 | } 84 | } 85 | 86 | return Boolean.TRUE; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/mstest_vs_2012.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | These are default test settings for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Assert.Fail failed. 41 | at TestProject1.UnitTest1.TestMethod2() in c:\users\carlos\documents\visual studio 2010\Projects\TestProject1\TestProject1\UnitTest1.cs:line 71 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/mstest_vs_2012_timeout.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | These are default test settings for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Assert.Fail failed. 41 | at TestProject1.UnitTest1.TestMethod2() in c:\users\carlos\documents\visual studio 2010\Projects\TestProject1\TestProject1\UnitTest1.cs:line 71 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/mstest_2_tests_from_different_assemblies.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is a default test run configuration for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Assert.AreEqual failed. Expected:<lala!>. Actual:<Lele!!>. 43 | at GoodGuyTest.GoodGuyTest.TestYell() in C:\PostIt\MultipleTests\GoodGuyTest\GoodGuyTest.cs:line 16 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/mstest_2_tests_1_class.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is a default test run configuration for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Assert.AreEqual failed. Expected:<2>. Actual:<1>. 49 | at FunctionsTest.FibonacciTest.CalculaFibonacci_2() in E:\Koiti\Dev\GitHudsonTest\FunctionsTest\FibonacciTest.cs:line 72 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/mstest_more_than_one_minute_test.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is a default test run configuration for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Assert.AreEqual failed. Expected:<2>. Actual:<1>. 49 | at FunctionsTest.FibonacciTest.CalculaFibonacci_2() in E:\Koiti\Dev\GitHudsonTest\FunctionsTest\FibonacciTest.cs:line 72 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/mstest_vs_2010.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | These are default test settings for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Assert.Fail failed. 41 | at TestProject1.UnitTest1.TestMethod2() in c:\users\carlos\documents\visual studio 2010\Projects\TestProject1\TestProject1\UnitTest1.cs:line 71 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/JENKINS-23531-xmlentities-forged.trx: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | This is a default test run configuration for a local test run. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 46 | 47 | 48 | Test method Common.Settings.Test.Status.Bb_StatusTest.Bb_GetLimitFixed_UserIsLoggedIn_Null 49 | threw exception: 50 | System.ArgumentException: Method 'GetLimitOption' not found on type 51 | Common.Settings.UserStatus.UserStatus 52 | 53 | 54 | at Telerik.JustMock.Expectations.NonPublicExpectation.?(Type ?, Type ?, 55 | String , Object[] ?) 56 | at Telerik.JustMock.Expectations.NonPublicExpectation.? .? () 57 | at ??.??.??[?](Func`1 ??) 58 | at Telerik.JustMock.Expectations.NonPublicExpectation.Arrange[TReturn](Type targetType, String 59 | memberName, Object[] args) 60 | at Common.Settings.Test.Status.Bb_StatusTest.Bb_GetLimitFixed_UserIsLoggedIn_Null() in 61 | c:\jenkins\workspace\dev\Common\Common.Settings.Test\BlackBox\UserStatus\Bb_UserStatus_GetLimitFixedTest.cs:line 62 | 32 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/JENKINS-23531-charset.trx: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | This is a default test run configuration for a local test run. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 46 | 47 | 48 | Test method Common.Settings.Test.Status.Bb_StatusTest.Bb_GetLimitFixed_UserIsLoggedIn_Null 49 | threw exception: 50 | System.ArgumentException: Method 'GetLimitOption' not found on type 51 | Common.Settings.UserStatus.UserStatus 52 | 53 | 54 | at Telerik.JustMock.Expectations.NonPublicExpectation.?(Type ?, Type ?, 55 | String , Object[] ?) 56 | at Telerik.JustMock.Expectations.NonPublicExpectation.? .? () 57 | at ??.??.??[?](Func`1 ??) 58 | at Telerik.JustMock.Expectations.NonPublicExpectation.Arrange[TReturn](Type targetType, String 59 | memberName, Object[] args) 60 | at Common.Settings.Test.Status.Bb_StatusTest.Bb_GetLimitFixed_UserIsLoggedIn_Null() in 61 | c:\jenkins\workspace\dev\Common\Common.Settings.Test\BlackBox\UserStatus\Bb_UserStatus_GetLimitFixedTest.cs:line 62 | 32 63 | éàèù 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci.plugins 5 | plugin 6 | 4.74 7 | 8 | 9 | 10 | 1.0.6 11 | -SNAPSHOT 12 | 2.401.3 13 | 14 | org.jvnet.hudson.plugins 15 | mstest 16 | hpi 17 | MSTest plugin 18 | Generates test reports for MSTest. 19 | ${revision}${changelist} 20 | http://wiki.jenkins-ci.org/display/JENKINS/MSTest+Plugin 21 | 22 | scm:git:https://github.com:jenkinsci/mstest-plugin.git 23 | scm:git:git@github.com:jenkinsci/mstest-plugin.git 24 | https://github.com/jenkinsci/mstest-plugin 25 | ${scmTag} 26 | 27 | 28 | 29 | nilleb 30 | Ivo Bellin Salarin 31 | me@nilleb.com 32 | 33 | 34 | casz 35 | Joseph Petersen 36 | https://github.com/casz/ama 37 | 38 | 39 | 40 | 41 | org.jenkins-ci.plugins 42 | junit 43 | 44 | 45 | 46 | 47 | xmlunit 48 | xmlunit 49 | 1.6 50 | test 51 | 52 | 53 | junit 54 | junit 55 | test 56 | 57 | 58 | org.jmock 59 | jmock-junit4 60 | 2.6.0 61 | test 62 | 63 | 64 | org.jmock 65 | jmock-legacy 66 | 2.6.0 67 | test 68 | 69 | 70 | cglib 71 | cglib 72 | 2.2.2 73 | test 74 | 75 | 76 | org.jenkins-ci.plugins.workflow 77 | workflow-job 78 | test 79 | 80 | 81 | org.jenkins-ci.plugins.workflow 82 | workflow-cps 83 | test 84 | 85 | 86 | org.jenkins-ci.plugins.workflow 87 | workflow-basic-steps 88 | test 89 | 90 | 91 | 92 | 93 | 94 | io.jenkins.tools.bom 95 | bom-2.401.x 96 | 2516.v113cb_3d00317 97 | import 98 | pom 99 | 100 | 101 | org.hamcrest 102 | hamcrest-core 103 | 2.1 104 | 105 | 106 | 107 | 108 | 109 | repo.jenkins-ci.org 110 | https://repo.jenkins-ci.org/public/ 111 | 112 | 113 | 114 | 115 | repo.jenkins-ci.org 116 | https://repo.jenkins-ci.org/public/ 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/mstest/MSTestTransformerAndConverterTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import hudson.FilePath; 4 | import hudson.model.TaskListener; 5 | import hudson.remoting.VirtualChannel; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.InputStream; 10 | import java.io.PrintStream; 11 | import java.nio.file.Files; 12 | import org.jmock.Expectations; 13 | import org.jmock.Mockery; 14 | import org.junit.After; 15 | import org.junit.Assert; 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | import org.springframework.util.FileCopyUtils; 19 | /* Java 1.7+ 20 | import java.nio.file.Files; 21 | */ 22 | 23 | /** 24 | * Unit tests for MSTestTransformer class 25 | * 26 | * @author Antonio Marques 27 | */ 28 | public class MSTestTransformerAndConverterTest extends TestHelper { 29 | 30 | 31 | private TaskListener buildListener; 32 | private Mockery context; 33 | private Mockery classContext; 34 | private MSTestTransformer transformer; 35 | private VirtualChannel virtualChannel; 36 | 37 | 38 | @Before 39 | public void setUp() throws Exception { 40 | createWorkspace(); 41 | 42 | context = getMock(); 43 | classContext = getClassMock(); 44 | buildListener = classContext.mock(TaskListener.class); 45 | virtualChannel = context.mock(VirtualChannel.class); 46 | } 47 | 48 | @After 49 | public void tearDown() throws Exception { 50 | deleteWorkspace(); 51 | } 52 | 53 | @Test 54 | public void testInvalidXmlCharacters() throws Exception { 55 | final PrintStream logger = new PrintStream(new ByteArrayOutputStream()); 56 | classContext.checking(new Expectations() { 57 | { 58 | ignoring(buildListener).getLogger(); 59 | will(returnValue(logger)); 60 | ignoring(buildListener); 61 | } 62 | }); 63 | final String testPath = "xmlentities-forged.trx"; 64 | File testFile = new File(parentFile, testPath); 65 | assert !testFile.exists() || testFile.delete(); 66 | InputStream testStream = this.getClass() 67 | .getResourceAsStream("JENKINS-23531-xmlentities-forged.trx"); 68 | Files.copy(testStream, testFile.toPath()); 69 | MSTestReportConverter converter = new MSTestReportConverter(buildListener); 70 | transformer = new MSTestTransformer(resolve(testPath), converter, buildListener, false); 71 | transformer.invoke(parentFile, virtualChannel); 72 | FilePath[] list = workspace.list("*.trx"); 73 | Assert.assertEquals(1, list.length); 74 | } 75 | 76 | @Test 77 | public void testInvalidXmlCharacters_VishalMane() throws Exception { 78 | final PrintStream logger = new PrintStream(new ByteArrayOutputStream()); 79 | classContext.checking(new Expectations() { 80 | { 81 | ignoring(buildListener).getLogger(); 82 | will(returnValue(logger)); 83 | ignoring(buildListener); 84 | } 85 | }); 86 | final String testPath = "vishalMane.trx"; 87 | File testFile = new File(parentFile, testPath); 88 | assert !testFile.exists() || testFile.delete(); 89 | InputStream testStream = this.getClass() 90 | .getResourceAsStream("SYSTEM_AD-JENKINS 2015-07-08 10_53_01.trx"); 91 | Files.copy(testStream, testFile.toPath()); 92 | MSTestReportConverter converter = new MSTestReportConverter(buildListener); 93 | transformer = new MSTestTransformer(resolve(testPath), converter, buildListener, false); 94 | transformer.invoke(parentFile, virtualChannel); 95 | FilePath[] list = workspace.list("*.trx"); 96 | Assert.assertEquals(1, list.length); 97 | } 98 | 99 | @Test 100 | public void testCharset() throws Exception { 101 | final PrintStream logger = new PrintStream(new ByteArrayOutputStream()); 102 | classContext.checking(new Expectations() { 103 | { 104 | ignoring(buildListener).getLogger(); 105 | will(returnValue(logger)); 106 | ignoring(buildListener); 107 | } 108 | }); 109 | final String testPath = "charset.trx"; 110 | File testFile = new File(parentFile, testPath); 111 | assert !testFile.exists() || testFile.delete(); 112 | InputStream testStream = this.getClass().getResourceAsStream("JENKINS-23531-charset.trx"); 113 | Files.copy(testStream, testFile.toPath()); 114 | MSTestReportConverter converter = new MSTestReportConverter(buildListener); 115 | transformer = new MSTestTransformer(resolve(testPath), converter, buildListener, false); 116 | transformer.invoke(parentFile, virtualChannel); 117 | FilePath[] list = workspace.list("*.trx"); 118 | Assert.assertEquals(1, list.length); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/DataDriven-no-tests-found.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | These are default test settings for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | The unit test adapter failed to connect to the data source or to read the data. For more information on troubleshooting this error, see "Troubleshooting Data-Driven Unit Tests" (http://go.microsoft.com/fwlink/?LinkId=62412) in the MSDN Library. 41 | Error details: 'E:\Jenkins\Please_Ignore_TestMSTest\controcctests-user_TYSHA 2016-07-18 13_58_43\Out\Nonexistentfolder' is not a valid path. Make sure that the path name is spelled correctly and that you are connected to the server on which the file resides. 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/ReportingOptions.2008.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | omit-from-jenkins:true 51 | 52 | 53 | 54 | 55 | test-alternate-name:Use This Name Instead: 56 | 57 | 58 | 59 | 60 | test-instance-name:My Suffix: 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/ReportingOptions.2013.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | omit-from-jenkins:true 43 | 44 | 45 | 46 | 47 | test-alternate-name:Use This Name Instead: 48 | 49 | 50 | 51 | 52 | test-instance-name:My Suffix: 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/JENKINS-25533+19384.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 17:43:23 ..Failed to connect: Attempt 0 46 | 17:43:25 ..Connected to CLIENT : v1.0.511.0 47 | 17:43:25 [PASS] Check Login dialog is presented on the main page 48 | 17:43:25 [PASS] Check OK button is disabled on initial Login Screen 49 | 17:43:27 ..Entered user name 50 | 17:43:28 ..Enter user password 51 | 17:43:28 [PASS] Check OK button became enabled after entered credentials 52 | 17:43:29 ..Confirmed login by OK button press 53 | 54 | Test method SanityCheck.Login threw exception: 55 | ArtOfTest.Common.Exceptions.AssertException: Assert.AreEqual - [Expected:True],[Actual:False] 56 | at ArtOfTest.Common.UnitTesting.Assert.AreEqual[T](T expected, T actual, String message) 57 | at ArtOfTest.Common.UnitTesting.Assert.AreEqual[T](T expected, T actual) 58 | at UITestAutomation.SimpleChecks.IsLoginDialogShown() in d:\tests_Workspace\src\TestAutomation\TestActions.cs:line 455 59 | at UITestAutomation.SimpleChecks.CheckLoginDialogShown(Boolean exp, String msg) in d:\tests_Workspace\src\TestAutomation\TestActions.cs:line 425 60 | at UITestAutomation.SimpleActions.LoginAs(String user, String password) in d:\tests_Workspace\src\TestAutomation\TestActions.cs:line 76 61 | at SanityCheck.Login() in d:\tests_Workspace\src\TestAutomation\SanityCheck.cs:line 25 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/mstest/MSTestPublisherJenkinsRuleTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package hudson.plugins.mstest; 7 | 8 | import hudson.EnvVars; 9 | import hudson.Launcher; 10 | import hudson.model.AbstractBuild; 11 | import hudson.model.BuildListener; 12 | import hudson.model.FreeStyleBuild; 13 | import hudson.model.FreeStyleProject; 14 | import hudson.slaves.EnvironmentVariablesNodeProperty; 15 | import java.io.File; 16 | import java.io.IOException; 17 | import org.apache.commons.io.FileUtils; 18 | import org.junit.Rule; 19 | import org.junit.Test; 20 | import org.jvnet.hudson.test.JenkinsRule; 21 | import org.jvnet.hudson.test.TestBuilder; 22 | 23 | import static org.junit.Assert.assertFalse; 24 | import static org.junit.Assert.assertTrue; 25 | 26 | /** 27 | * @author nilleb 28 | */ 29 | public class MSTestPublisherJenkinsRuleTest { 30 | 31 | @Rule 32 | public JenkinsRule j = new JenkinsRule(); 33 | 34 | private MSTestPublisher getTestedPublisher(String pattern) { 35 | MSTestPublisher publisher = new MSTestPublisher(); 36 | publisher.setTestResultsFile(pattern); 37 | publisher.setFailOnError(false); 38 | publisher.setKeepLongStdio(false); 39 | return publisher; 40 | } 41 | 42 | @Test 43 | public void testResolveEnvironmentVariables() throws Exception { 44 | EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); 45 | EnvVars envVars = prop.getEnvVars(); 46 | envVars.put("TRX", "build.trx"); 47 | j.jenkins.getGlobalNodeProperties().add(prop); 48 | FreeStyleProject project = j.createFreeStyleProject(); 49 | project.getPublishersList().add(getTestedPublisher("$TRX")); 50 | FreeStyleBuild build = project.scheduleBuild2(0).get(); 51 | 52 | if (build != null) { 53 | String s = FileUtils.readFileToString(build.getLogFile()); 54 | assertFalse(s.contains("Processing tests results in file(s) $TRX")); 55 | assertTrue(s.contains("build.trx")); 56 | } 57 | } 58 | 59 | @Test 60 | public void testResolveMultipleEnvironmentVariables() throws Exception { 61 | EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); 62 | EnvVars envVars = prop.getEnvVars(); 63 | envVars.put("TRX", "build.trx"); 64 | j.jenkins.getGlobalNodeProperties().add(prop); 65 | FreeStyleProject project = j.createFreeStyleProject(); 66 | project.getPublishersList().add(getTestedPublisher("$WORKSPACE/$TRX")); 67 | FreeStyleBuild build = project.scheduleBuild2(0).get(); 68 | 69 | if (build != null) { 70 | String s = FileUtils.readFileToString(build.getLogFile()); 71 | assertFalse(s.contains("Processing tests results in file(s) $TRX")); 72 | assertTrue(s.contains("/build.trx")); 73 | } 74 | } 75 | 76 | @Test 77 | public void testExecuteOnRealTrx() throws Exception { 78 | EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); 79 | EnvVars envVars = prop.getEnvVars(); 80 | envVars.put("TRX", "results-example-mstest.trx"); 81 | j.jenkins.getGlobalNodeProperties().add(prop); 82 | FreeStyleProject project = j.createFreeStyleProject(); 83 | project.getPublishersList().add(getTestedPublisher("$WORKSPACE/$TRX")); 84 | project.getBuildersList().add(new TestBuilder() { 85 | public boolean perform(AbstractBuild build, Launcher launcher, 86 | BuildListener listener) throws InterruptedException, IOException { 87 | File f = new File( 88 | "src/test/resources/hudson/plugins/mstest/results-example-mstest.trx"); 89 | assertTrue(f.exists()); 90 | File dest = new File(build.getWorkspace().getRemote()); 91 | assertTrue(dest.exists()); 92 | FileUtils.copyFileToDirectory(f, dest); 93 | assertTrue(build.getWorkspace().child("results-example-mstest.trx").exists()); 94 | return true; 95 | } 96 | }); 97 | 98 | FreeStyleBuild build = project.scheduleBuild2(0).get(); 99 | if (build != null) { 100 | String s = FileUtils.readFileToString(build.getLogFile()); 101 | assertTrue(s.contains(File.separator + "results-example-mstest.trx")); 102 | } 103 | } 104 | 105 | @Test 106 | public void testExecuteOnRealTrx_usingAntFileSet() throws Exception { 107 | FreeStyleProject project = j.createFreeStyleProject(); 108 | project.getPublishersList().add(getTestedPublisher("**/*.trx")); 109 | assertTrue(project.getBuildersList().add(new TestBuilder() { 110 | public boolean perform(AbstractBuild build, Launcher launcher, 111 | BuildListener listener) throws InterruptedException, IOException { 112 | File f = new File( 113 | "src/test/resources/hudson/plugins/mstest/results-example-mstest.trx"); 114 | assertTrue(f.exists()); 115 | File dest = new File(build.getWorkspace().getRemote(), 116 | "TestResults_51310ef0-5d36-47cc-a1a9-b21d6f3e2072"); 117 | assertTrue(dest.mkdir()); 118 | FileUtils.copyFileToDirectory(f, dest); 119 | assertTrue(build.getWorkspace().child( 120 | "TestResults_51310ef0-5d36-47cc-a1a9-b21d6f3e2072/results-example-mstest.trx") 121 | .exists()); 122 | return true; 123 | } 124 | })); 125 | 126 | FreeStyleBuild build = project.scheduleBuild2(0).get(); 127 | if (build != null) { 128 | String s = FileUtils.readFileToString(build.getLogFile()); 129 | assertTrue(s == null || s.contains(File.separator + "results-example-mstest.trx")); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/kozl-unit-tests-missing.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | These are default test settings for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Method LindbergCorp.StringFileTranslator.Test.TranslatorTests.TestFixtureSetup has wrong signature. The method should have 1 parameters. 52 | 53 | 54 | 55 | 56 | 57 | 58 | Method LindbergCorp.StringFileTranslator.Test.TranslatorTests.TestFixtureSetup has wrong signature. The method should have 1 parameters. 59 | 60 | 61 | 62 | 63 | 64 | 65 | Method LindbergCorp.StringFileTranslator.Test.TranslatorTests.TestFixtureSetup has wrong signature. The method should have 1 parameters. 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/webTestResult.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | These are default test settings for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | These are default test settings for a local test run. 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 |
71 |
72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | xxxxxxxxxxxxxxxxxx\Login - Sucess.webtestResult 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/test/resources/hudson/plugins/mstest/mstest_4_tests_2_classes.trx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is a default test run configuration for a local test run. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Assert.AreEqual failed. Expected:<2>. Actual:<1>. 67 | at FunctionsTest.FibonacciTest.CalculaFibonacci_2() in E:\Koiti\Dev\GitHudsonTest\FunctionsTest\FibonacciTest.cs:line 72 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Assert.AreEqual failed. Expected:<pao>. Actual:<paao>. 80 | at FunctionsTest.SpecialCharRemoverTest.REmoveSpecialChar2() in E:\Koiti\Dev\GitHudsonTest\FunctionsTest\SpecialCharRemoverTest.cs:line 76 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/vstest.console2codecoverage.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | yes 76 | partial 77 | no 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/main/webapp/help.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | The MSTest plugin can convert TRX format test reports into JUnit XML format so it can be recorded 4 | by Hudson. 5 |

6 | 7 |

General Usage

8 |

To enable the MSTest plugin features you need to set up your build to run MSTest.exe (or VSTest.Console.exe) first. Example: 9 |

    "%PROGRAMFILES%\Microsoft Visual Studio 9.0\Common7\IDE\MSTest.exe" /runconfig:MyTestProject\LocalTestRun.testrunconfig /testcontainer:MyTestProject\bin\Release\MyTestProject.dll /resultsfile:TestResults\testResults.trx 10 |
11 | 12 | and then specify the path to the MSTest TRX file, TestResults/testResults.trx in the above example. 13 |

14 |

15 | When using VSTest.Console.exe, you can't specify the path to the resulting TRX file. Thus, you can specify a pattern like TestResults/*/*.trx (/ or \, either will work). 16 |

17 |

18 | If you use the vstestrunner-plugin, the full path to the TRX file is exposed by an environment variable. So, you can also use an environment variable, 19 | like $VSTEST_RESULT_TRX or ${VSTEST_RESULT_TRX}. 20 |

21 |

Test Reporting Options

22 |

23 | Test authors can control how each test appears in Hudson/Jenkins by writing specially formatted text to stdout (the console) at the beginning of the test method. The following options are available: 24 |

  • Append text to the test name (useful for identifying individual data-driven test instances)
  • 25 |
  • Omit a test from Hudson/Jenkins (useful for some data-driven test scenarios)
  • 26 |
  • Change the test's display name from the name of the test method to an alternate name (useful when reporting results to non-developers)
  • 27 |
28 | In all cases, the specially formatted text must be the first text to appear in the individual test's stdout. 29 |

30 |

Appending to a test's display name (naming each data-driven test instance)

31 |

32 | You may want a test to appear in Jenkins with additional text appended to the usual name of the test method. This is 33 | particularly useful for data-driven tests, allowing you to give each individual test case an identifying suffix. To change a test's 34 | display name, write the following line as the first text written to stdout during the test's execution: 35 | 36 |

    test-instance-name:{test instance name}:
37 | 38 | Replace "{test instance name}" with whatever text you want to have appended to the test name in test reports. For example, if a 39 | test method is named MyTestMethod, and the first line of a particular execution of that test method is 40 | test-instance-name:Test Case A:, that instance of the test will be displayed in Jenkins as MyTestMethod.Test Case A. 41 | Colons are used to delimit the test instance name, so the instance name itself must not contain a colon. 42 |

43 |

44 | This option is particularly useful if you store the test case name as part of each data row in the data source. You can then refer to 45 | the name in the test itself: 46 | 47 |

    Console.WriteLine(String.Format("test-instance-name:{0}:", data_row("TestCaseName")))
48 | 49 | You could also use the values of the input parameters themselves as the test instance name: 50 | 51 |
    Console.WriteLine(String.Format("test-instance-name:{0}, {1}:", data_row("param1"), data_row("param2")))
52 | 53 | Note that if you don't specify a test-instance-name, then data driven tests are automatically given a suffix based on 54 | the data row ID (such as "row 3"), similar to what is displayed in Visual Studio. 55 |

56 |

57 | Be aware that TRX files generated by vstest.console.exe differ slightly from those generated by mstest, and these differences 58 | affect how data-driven test names appear in test reports. Specifically, data-driven tests appear with their data 59 | row number, even if you append a test instance name. For example, an instance of the test MyTestMethod with the instance 60 | name Test Case A will be displayed as MyTestMethod.Test Case A if the TRX file was generated by mstest, and 61 | as MyTestMethod (Data Row 0).Test Case A if the TRX file was generated by vstest.console.exe. 62 |

63 | 64 |

Omitting a test from the test report

65 |

66 | You may want a test to be removed from the test report. This is useful in some data-driven test scenarios. For example, consider a 67 | set of input parameters in a data source, each of which will be provided to multiple tests. Some inputs may not apply to certain tests. 68 | The testing framework will run each test against every available data row, even if the input doesn't apply to a particular test. Removing 69 | irrelevant test cases from the test report is cleaner and gives more accurate statistics. The syntax to omit a test is similar to other 70 | reporting options: 71 | 72 |

    omit-from-jenkins:true
73 | 74 | Like the other reporting options, this must be the first line of text written to stdout during the execution of the test you want to filter 75 | from the test report. 76 |

77 |

Example

78 |

79 | Here is an example of how this capability might be used. Consider a data source with the following data: 80 | 81 |

    82 | 83 | 84 | 85 | 86 |
    TestCaseNameParam1ExpectedResult1ExpectedResult2
    Case A"Input1"3.11.7
    Case B"Input2"0.91.0
    Case C"Input3"4.2
87 | 88 | In this example, Test1 applies to all 3 data rows, with the expected result specified as ExpectedResult1. Test2, however, only applies 89 | to the first 2 data rows; the test does not apply to Case C, as indicated by the blank ExpectedResult2 for that data row. The Test2 test 90 | method will need to recognize which test cases to ignore, and write reporting instructions accordingly: 91 | 92 |
     93 |      [DataSource("blah blah blah")]
     94 |      [TestMethod()]
     95 |      public void Test2()
     96 |      {
     97 |         if (testContext.DataRow("ExpectedResult2") == null) {
     98 |             Console.WriteLine("omit-from-jenkins:true");
     99 |             return;
    100 |         }
    101 | 
    102 |         Console.WriteLine(String.Format("test-instance-name:{0}:", testContext.DataRow("TestCaseName")));
    103 |         // do test logic
    104 |         ...
    105 |      }  
    106 |      
107 |

108 | 109 |

Renaming a test

110 |

111 | You may want a test to appear in Jenkins with a different name than the name of the test method, possibly to provide a more 112 | readable name than the name of the test method. To change a test's display name, write the following line as the first text 113 | written to stdout during the test's execution: 114 | 115 |

    test-alternate-name:{alternate name}:
116 | 117 | Replace "{alternate name}" with whatever text you want to use as the name of the test in Jenkins reports. For example, 118 | test-alternate-name:My Test: will be displayed in Jenkins as My Test, regardless of the name 119 | of the test's test method. Colons are used to delimit the alternate test name, so the alternate name itself must not contain 120 | a colon. 121 |

122 |

123 | The test-alternate-name option may not be combined with any other reporting option. If you want to rename a 124 | test and append a suffix, include the suffix in the name supplied to the test-alternate-name directive. 125 |

126 | 127 |
128 | 129 | 130 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/mstest/MSTestPublisherTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import hudson.EnvVars; 4 | import hudson.FilePath; 5 | import hudson.Launcher; 6 | import hudson.model.AbstractProject; 7 | import hudson.model.Action; 8 | import hudson.model.Result; 9 | import hudson.model.Run; 10 | import hudson.model.TaskListener; 11 | import hudson.tasks.junit.TestResult; 12 | import hudson.tasks.junit.TestResultAction; 13 | import hudson.tasks.test.TestResultProjectAction; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertNotNull; 17 | 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.File; 20 | import java.io.FileOutputStream; 21 | import java.io.InputStream; 22 | import java.io.PrintStream; 23 | import java.util.GregorianCalendar; 24 | 25 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 26 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 27 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 28 | import org.jmock.Expectations; 29 | import org.jmock.Mockery; 30 | import org.junit.After; 31 | import org.junit.Assert; 32 | import org.junit.Before; 33 | import org.junit.Ignore; 34 | import org.junit.Rule; 35 | import org.junit.Test; 36 | import org.jvnet.hudson.test.JenkinsRule; 37 | import org.springframework.util.FileCopyUtils; 38 | 39 | /** 40 | * Unit tests for MSTestPublisher class 41 | * 42 | * @author Antonio Marques 43 | */ 44 | public class MSTestPublisherTest extends TestHelper { 45 | 46 | @Rule 47 | public JenkinsRule j = new JenkinsRule(); 48 | 49 | private Mockery classContext; 50 | private AbstractProject project; 51 | private TaskListener buildListener; 52 | private Run run; 53 | 54 | @Before 55 | public void setUp() throws Exception { 56 | createWorkspace(); 57 | classContext = getClassMock(); 58 | project = classContext.mock(AbstractProject.class); 59 | buildListener = classContext.mock(TaskListener.class); 60 | run = classContext.mock(Run.class); 61 | } 62 | 63 | @After 64 | public void tearDown() throws Exception { 65 | deleteWorkspace(); 66 | } 67 | 68 | private MSTestPublisher getTestedPublisher(String pattern) { 69 | MSTestPublisher publisher = new MSTestPublisher(); 70 | publisher.setTestResultsFile(pattern); 71 | publisher.setFailOnError(false); 72 | publisher.setKeepLongStdio(false); 73 | return publisher; 74 | } 75 | 76 | @Test 77 | public void testGetProjectActionProjectReusing() { 78 | classContext.checking(new Expectations() { 79 | { 80 | oneOf(project).getAction(with(equal(TestResultProjectAction.class))); 81 | will(returnValue(new TestResultProjectAction(project))); 82 | } 83 | }); 84 | MSTestPublisher publisher = getTestedPublisher("build.trx"); 85 | Action projectAction = publisher.getProjectAction(project); 86 | Assert.assertNull("The action was not null", projectAction); 87 | } 88 | 89 | @Test 90 | public void testGetProjectActionProject() { 91 | classContext.checking(new Expectations() { 92 | { 93 | oneOf(project).getAction(with(equal(TestResultProjectAction.class))); 94 | will(returnValue(null)); 95 | } 96 | }); 97 | MSTestPublisher publisher = getTestedPublisher("build.trx"); 98 | Action projectAction = publisher.getProjectAction(project); 99 | Assert.assertNotNull("The action was null", projectAction); 100 | Assert.assertEquals("The action type is incorrect", TestResultProjectAction.class, 101 | projectAction.getClass()); 102 | } 103 | 104 | @Test 105 | public void testNoFileMatchingPattern() throws Exception { 106 | classContext.checking(new Expectations() { 107 | { 108 | oneOf(run).getEnvironment(with(equal(buildListener))); 109 | will(returnValue(new EnvVars())); 110 | } 111 | }); 112 | classContext.checking(new Expectations() { 113 | { 114 | ignoring(buildListener).getLogger(); 115 | will(returnValue(new PrintStream(new ByteArrayOutputStream()))); 116 | oneOf(buildListener).fatalError(with(any(String.class))); 117 | } 118 | }); 119 | File subfolder = new File(parentFile, "subfolder"); 120 | assert subfolder.exists() || subfolder.mkdirs(); 121 | File testFile = new File(subfolder, "xmlentities-forged.trx"); 122 | assert !testFile.exists() || testFile.delete(); 123 | InputStream testStream = this.getClass() 124 | .getResourceAsStream("JENKINS-23531-xmlentities-forged.trx"); 125 | FileCopyUtils.copy(testStream, new FileOutputStream(testFile)); 126 | String[] results = MSTestPublisher 127 | .resolveTestReports("*.trx", run, workspace, buildListener); 128 | Assert.assertEquals(0, results.length); 129 | } 130 | 131 | @Ignore 132 | @Test 133 | public void testComplete() throws Exception { 134 | classContext.checking(new Expectations() { 135 | { 136 | ignoring(run).getEnvironment(with(equal(buildListener))); 137 | will(returnValue(new EnvVars())); 138 | } 139 | }); 140 | classContext.checking(new Expectations() { 141 | { 142 | ignoring(run).getTimestamp(); 143 | GregorianCalendar c = new GregorianCalendar(); 144 | c.setTimeInMillis(0L); 145 | will(returnValue(c)); 146 | } 147 | }); 148 | classContext.checking(new Expectations() { 149 | { 150 | ignoring(buildListener).getLogger(); 151 | will(returnValue(new PrintStream(new ByteArrayOutputStream()))); 152 | oneOf(buildListener).fatalError(with(any(String.class))); 153 | } 154 | }); 155 | classContext.checking(new Expectations() { 156 | { 157 | oneOf(run).getAction(with(equal(TestResultAction.class))); 158 | will(returnValue(null)); 159 | } 160 | }); 161 | classContext.checking(new Expectations() { 162 | { 163 | oneOf(run).getPreviousBuild(); 164 | will(returnValue(null)); 165 | } 166 | }); 167 | classContext.checking(new Expectations() { 168 | { 169 | oneOf(run).getNumber(); 170 | will(returnValue(1)); 171 | } 172 | }); 173 | classContext.checking(new Expectations() { 174 | { 175 | new TestResultAction(with(run), with(new TestResult()), with(buildListener)); 176 | will(returnValue(new TestResultAction(run, null, buildListener))); 177 | } 178 | }); 179 | 180 | File subfolder = new File(parentFile, "subfolder"); 181 | assert subfolder.exists() || subfolder.mkdirs(); 182 | File testFile = new File(subfolder, "xmlentities-forged.trx"); 183 | assert !testFile.exists() || testFile.delete(); 184 | InputStream testStream = this.getClass() 185 | .getResourceAsStream("JENKINS-23531-xmlentities-forged.trx"); 186 | FileCopyUtils.copy(testStream, new FileOutputStream(testFile)); 187 | MSTestPublisher publisher = getTestedPublisher("**/*.trx"); 188 | Launcher launcher = classContext.mock(Launcher.class); 189 | publisher.perform(run, workspace, launcher, buildListener); 190 | } 191 | 192 | @Test 193 | public void simplePipelinePublishing() throws Exception { 194 | WorkflowJob job = j.createProject(WorkflowJob.class, "simplePipelinePublishing"); 195 | FilePath ws = j.jenkins.getWorkspaceFor(job); 196 | 197 | FilePath testFile = ws.child("build.trx"); 198 | testFile.copyFrom(this.getClass().getResourceAsStream("webTestResult.trx")); 199 | 200 | job.setDefinition(new CpsFlowDefinition("node {\n" + 201 | " mstest(testResultsFile: 'build.trx')" + 202 | "}\n", true 203 | )); 204 | WorkflowRun r = j.waitForCompletion(job.scheduleBuild2(0).waitForStart()); 205 | j.assertBuildStatus(Result.SUCCESS, r); 206 | 207 | // Asser action 208 | TestResultAction action = r.getAction(TestResultAction.class); 209 | assertNotNull(action); 210 | assertEquals(1, action.getTotalCount()); 211 | assertEquals(0, action.getSkipCount()); 212 | assertEquals(0, action.getFailCount()); 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/mstest/mstest-to-junit.xsl: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 0 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | . 61 | 62 | row 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | failure 117 | skipped 118 | error 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/mstest/MSTestReportConverter.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import hudson.model.TaskListener; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.Serializable; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import javax.xml.XMLConstants; 12 | import javax.xml.parsers.DocumentBuilder; 13 | import javax.xml.parsers.DocumentBuilderFactory; 14 | import javax.xml.parsers.ParserConfigurationException; 15 | import javax.xml.transform.TransformerConfigurationException; 16 | import javax.xml.transform.TransformerException; 17 | import javax.xml.transform.TransformerFactoryConfigurationError; 18 | import javax.xml.transform.dom.DOMSource; 19 | import javax.xml.xpath.XPath; 20 | import javax.xml.xpath.XPathConstants; 21 | import javax.xml.xpath.XPathExpression; 22 | import javax.xml.xpath.XPathExpressionException; 23 | import javax.xml.xpath.XPathFactory; 24 | import org.apache.commons.io.FilenameUtils; 25 | import org.w3c.dom.Document; 26 | import org.w3c.dom.Element; 27 | import org.w3c.dom.NodeList; 28 | import org.xml.sax.SAXException; 29 | 30 | /** 31 | * Transforms a MSTest report into a JUnit report by means of a XSL transform. Converts all the 32 | * coverage reports referenced by this TRX to a emma compatible format. 33 | */ 34 | class MSTestReportConverter implements Serializable { 35 | 36 | static final String MSTEST_TO_JUNIT_XSLFILE_STR = "mstest-to-junit.xsl"; 37 | private static final long serialVersionUID = 1L; 38 | private static final String JUNIT_FILE_POSTFIX = ".xml"; 39 | private static final String JUNIT_FILE_PREFIX = "TEST-"; 40 | private static final String TEMP_JUNIT_FILE_STR = "temp-junit.xml"; 41 | private static final String MSTESTCOVERAGE_TO_EMMA_XSLFILE_STR = "MSTestCoverageToEmma.xsl"; 42 | private static final String EMMA_FILE_STR = "emma" + File.separator + "coverage.xml"; 43 | private static final String MSTESTCOVERAGE_FILE_STR = "vstest.coveragexml"; 44 | private static final String MSTESTCOVERAGE_FILE_EXT = ".coveragexml"; 45 | 46 | private MsTestLogger logger; 47 | private transient int fileCount; 48 | 49 | MSTestReportConverter(TaskListener listener) { 50 | this.logger = new MsTestLogger(listener); 51 | } 52 | 53 | /** 54 | * Transform the MSTest TRX file into a junit XML file in the output path 55 | * 56 | * @param file the mstest file to transform 57 | * @param junitOutputPath the output path to put all junit files 58 | * @throws java.io.FileNotFoundException if the input file doesn't exist 59 | * @throws IOException thrown if there was any problem with the transform. 60 | * @throws TransformerException thrown if the XSL stylesheet is invalid 61 | * @throws SAXException thrown if the input XML file is invalid 62 | * @throws ParserConfigurationException thrown if something is wrong with the current system XL 63 | * configuration 64 | */ 65 | void transform(String file, File junitOutputPath) 66 | throws IOException, TransformerException, 67 | SAXException, ParserConfigurationException { 68 | File f = new File(file); 69 | try (FileInputStream fileStream = new FileInputStream(f)) { 70 | transform(fileStream, junitOutputPath); 71 | } 72 | 73 | for (File c : getCoverageFiles(f)) { 74 | if (c.exists()) { 75 | if (containsData(c)) { 76 | convertToEmma(f, c); 77 | break; 78 | } else { 79 | logger 80 | .warn("XML coverage report file format not supported (read the wiki): %s\n", 81 | c.getAbsolutePath()); 82 | } 83 | } else { 84 | logger.info("XML coverage report file not found: %s\n", c.getAbsolutePath()); 85 | } 86 | } 87 | } 88 | 89 | private List getCoverageFiles(File trxFile) { 90 | List coverageFiles = new ArrayList<>(); 91 | coverageFiles.add(new File(trxFile.getParent(), MSTESTCOVERAGE_FILE_STR)); 92 | coverageFiles.add(getCoverageFile(trxFile)); 93 | return coverageFiles; 94 | } 95 | 96 | private File getCoverageFile(File trxFile) { 97 | String fileNameWithOutExt = FilenameUtils 98 | .removeExtension(FilenameUtils.getBaseName(trxFile.getAbsolutePath())); 99 | return new File(trxFile.getParentFile(), fileNameWithOutExt + MSTESTCOVERAGE_FILE_EXT); 100 | } 101 | 102 | private void convertToEmma(File f, File c) 103 | throws TransformerException, IOException, ParserConfigurationException { 104 | File emmaTargetFile = new File(f.getParent(), EMMA_FILE_STR); 105 | FileOperator.safeCreateFolder(emmaTargetFile.getParentFile(), logger); 106 | logger.info("XML coverage: transforming '%s' to '%s'\n", c.getAbsolutePath(), 107 | emmaTargetFile.getAbsolutePath()); 108 | try (FileInputStream fileStream = new FileInputStream(c)) { 109 | XslTransformer.FromResource(MSTESTCOVERAGE_TO_EMMA_XSLFILE_STR) 110 | .transform(fileStream, emmaTargetFile); 111 | } 112 | } 113 | 114 | private boolean containsData(File c) throws IOException { 115 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 116 | try { 117 | factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 118 | factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 119 | factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 120 | factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 121 | 122 | factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 123 | factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 124 | 125 | DocumentBuilder builder = factory.newDocumentBuilder(); 126 | Document doc = builder.parse(c); 127 | XPathFactory xPathfactory = XPathFactory.newInstance(); 128 | XPath xpath = xPathfactory.newXPath(); 129 | XPathExpression expr = xpath.compile("count(/CoverageDSPriv/*)"); 130 | Double childCount = (Double) expr.evaluate(doc, XPathConstants.NUMBER); 131 | return childCount > 0; 132 | } catch (ParserConfigurationException | SAXException | XPathExpressionException ex) { 133 | MsTestLogger.getLogger() 134 | .error("Caught a XML parsing related exception: %s", ex.getMessage()); 135 | } 136 | return false; 137 | } 138 | 139 | /** 140 | * Transform the MSTest TRX file into a junit XML file in the output path 141 | * 142 | * @param mstestFileStream the mstest file stream to transform 143 | * @param junitOutputPath the output path to put all junit files 144 | * @throws IOException thrown if there was any problem with the transform. 145 | */ 146 | private void transform(InputStream mstestFileStream, File junitOutputPath) 147 | throws IOException, TransformerException, 148 | SAXException, ParserConfigurationException { 149 | File junitTargetFile = new File(junitOutputPath, TEMP_JUNIT_FILE_STR); 150 | XslTransformer.FromResource(MSTEST_TO_JUNIT_XSLFILE_STR) 151 | .transform(mstestFileStream, junitTargetFile); 152 | splitJUnitFile(junitTargetFile, junitOutputPath); 153 | FileOperator.safeDelete(junitOutputPath, logger); 154 | } 155 | 156 | private DocumentBuilder getDocumentBuilder() 157 | throws TransformerFactoryConfigurationError, 158 | ParserConfigurationException { 159 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 160 | factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 161 | factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 162 | factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 163 | factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 164 | factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 165 | factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 166 | return factory.newDocumentBuilder(); 167 | } 168 | 169 | /** 170 | * Splits the junit file into several junit files in the output path 171 | * 172 | * @param junitFile report containing one or more junit test suite tags 173 | * @param junitOutputPath the path to put all junit files 174 | */ 175 | private void splitJUnitFile(File junitFile, File junitOutputPath) 176 | throws SAXException, IOException, 177 | TransformerException, TransformerFactoryConfigurationError, ParserConfigurationException { 178 | Document document = getDocumentBuilder().parse(junitFile); 179 | 180 | NodeList elementsByTagName = ((Element) document.getElementsByTagName("testsuites").item(0)) 181 | .getElementsByTagName("testsuite"); 182 | for (int i = 0; i < elementsByTagName.getLength(); i++) { 183 | Element element = (Element) elementsByTagName.item(i); 184 | DOMSource source = new DOMSource(element); 185 | 186 | String filename = JUNIT_FILE_PREFIX + (fileCount++) + JUNIT_FILE_POSTFIX; 187 | File junitOutputFile = new File(junitOutputPath, filename); 188 | try { 189 | new XslTransformer().transform(source, junitOutputFile); 190 | } catch (TransformerConfigurationException ex) { 191 | MsTestLogger.getLogger().error( 192 | "Caught a TransformerConfigurationException (what's the system configuration?) %s", 193 | ex.getMessage()); 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/mstest/MSTestPublisher.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.mstest; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.AbortException; 5 | import hudson.Extension; 6 | import hudson.FilePath; 7 | import hudson.Launcher; 8 | import hudson.Util; 9 | import hudson.model.AbstractBuild; 10 | import hudson.model.AbstractProject; 11 | import hudson.model.Action; 12 | import hudson.model.BuildListener; 13 | import hudson.model.Job; 14 | import hudson.model.Result; 15 | import hudson.model.Run; 16 | import hudson.model.TaskListener; 17 | import hudson.remoting.VirtualChannel; 18 | import hudson.tasks.BuildStepDescriptor; 19 | import hudson.tasks.BuildStepMonitor; 20 | import hudson.tasks.Publisher; 21 | import hudson.tasks.Recorder; 22 | import hudson.tasks.junit.TestResult; 23 | import hudson.tasks.junit.TestResultAction; 24 | import hudson.tasks.test.TestResultProjectAction; 25 | import java.io.File; 26 | import java.io.IOException; 27 | import java.io.Serializable; 28 | import java.util.ArrayList; 29 | import java.util.Collection; 30 | import jenkins.MasterToSlaveFileCallable; 31 | import jenkins.tasks.SimpleBuildStep; 32 | import org.apache.tools.ant.DirectoryScanner; 33 | import org.apache.tools.ant.types.FileSet; 34 | import org.jenkinsci.Symbol; 35 | import org.kohsuke.stapler.DataBoundConstructor; 36 | import org.kohsuke.stapler.DataBoundSetter; 37 | 38 | /** 39 | * Class that records MSTest test reports into Hudson. 40 | * 41 | * @author Antonio Marques 42 | */ 43 | public class MSTestPublisher extends Recorder implements Serializable, SimpleBuildStep { 44 | 45 | private static final long serialVersionUID = 1L; 46 | @NonNull 47 | private String testResultsFile = DescriptorImpl.defaultTestResultsFile; 48 | private boolean failOnError = DescriptorImpl.defaultFailOnError; 49 | private boolean keepLongStdio = DescriptorImpl.defaultKeepLongStdio; 50 | private String logLevel = DescriptorImpl.defaultLogLevel; 51 | private long buildTime; 52 | 53 | @DataBoundConstructor 54 | public MSTestPublisher() { 55 | } 56 | 57 | static String[] resolveTestReports(String testReportsPattern, @NonNull Run build, 58 | @NonNull FilePath workspace, @NonNull TaskListener listener) { 59 | FileResolver resolver = new FileResolver(listener); 60 | String resolved = resolver.SafeResolveFilePath(testReportsPattern, build, listener); 61 | return resolver.FindMatchingMSTestReports(resolved, workspace); 62 | } 63 | 64 | @NonNull 65 | public String getTestResultsFile() { 66 | return testResultsFile; 67 | } 68 | 69 | @DataBoundSetter 70 | public final void setTestResultsFile(@NonNull String testResultsFile) { 71 | this.testResultsFile = testResultsFile; 72 | } 73 | 74 | public boolean getFailOnError() { 75 | return failOnError; 76 | } 77 | 78 | @DataBoundSetter 79 | public final void setFailOnError(boolean failOnError) { 80 | this.failOnError = failOnError; 81 | } 82 | 83 | public boolean getKeepLongStdio() { 84 | return keepLongStdio; 85 | } 86 | 87 | @DataBoundSetter 88 | public final void setKeepLongStdio(boolean keepLongStdio) { 89 | this.keepLongStdio = keepLongStdio; 90 | } 91 | 92 | public String getLogLevel() { return logLevel; } 93 | 94 | @DataBoundSetter 95 | public final void setLogLevel(String logLevel) { this.logLevel = logLevel; } 96 | 97 | @Override 98 | public Action getProjectAction(AbstractProject project) { 99 | TestResultProjectAction action = project.getAction(TestResultProjectAction.class); 100 | if (action == null) { 101 | return new TestResultProjectAction((Job) project); 102 | } else { 103 | return null; 104 | } 105 | } 106 | 107 | @Override 108 | @NonNull 109 | public Collection getProjectActions(AbstractProject project) { 110 | Collection actions = new ArrayList<>(); 111 | Action action = this.getProjectAction(project); 112 | if (action != null) { 113 | actions.add(action); 114 | } 115 | return actions; 116 | } 117 | 118 | public BuildStepMonitor getRequiredMonitorService() { 119 | return BuildStepMonitor.NONE; 120 | } 121 | 122 | @Override 123 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) 124 | throws InterruptedException, IOException { 125 | final FilePath workspace = build.getWorkspace(); 126 | 127 | if (workspace == null) { 128 | throw new IllegalArgumentException(); 129 | } 130 | 131 | perform(build, workspace, launcher, listener); 132 | return true; 133 | } 134 | 135 | @Override 136 | public void perform(final @NonNull Run build, @NonNull FilePath workspace, 137 | @NonNull Launcher launcher, final @NonNull TaskListener listener) 138 | throws InterruptedException, IOException { 139 | 140 | System.setProperty(MsTestLogger.HUDSON_PLUGINS_MSTEST_LEVEL, this.logLevel); 141 | 142 | buildTime = build.getTimestamp().getTimeInMillis(); 143 | 144 | String[] matchingFiles = resolveTestReports(testResultsFile, build, workspace, listener); 145 | MSTestReportConverter converter = new MSTestReportConverter(listener); 146 | MSTestTransformer transformer = new MSTestTransformer(matchingFiles, converter, listener, 147 | failOnError); 148 | boolean result = workspace.act(transformer); 149 | 150 | if (result) { 151 | // Run the JUnit test archiver 152 | recordTestResult(MSTestTransformer.JUNIT_REPORTS_PATH + "/TEST-*.xml", build, workspace, 153 | listener); 154 | workspace.child(MSTestTransformer.JUNIT_REPORTS_PATH).deleteRecursive(); 155 | } else { 156 | throw new AbortException("Unable to transform the MSTest report."); 157 | } 158 | } 159 | 160 | /** 161 | * Record the test results into the current build. 162 | * 163 | * @param junitFilePattern the ant file pattern mathing the junit test results file 164 | * @param build the current build 165 | * @param listener the log listener 166 | * @throws InterruptedException workspace/jenkins operations may throw 167 | * @throws IOException workspace/jenkins operations may throw 168 | */ 169 | private void recordTestResult(String junitFilePattern, Run build, FilePath workspace, 170 | TaskListener listener) 171 | throws InterruptedException, IOException { 172 | TestResultAction existingAction = build.getAction(TestResultAction.class); 173 | TestResultAction action; 174 | 175 | MsTestLogger logger = new MsTestLogger(listener); 176 | TestResult existingTestResults = null; 177 | 178 | if (existingAction != null) { 179 | existingTestResults = existingAction.getResult(); 180 | } 181 | TestResult result = getTestResult(junitFilePattern, workspace, existingTestResults); 182 | 183 | if (result == null) { 184 | return; 185 | } 186 | 187 | if (existingAction == null) { 188 | action = new TestResultAction(build, result, listener); 189 | } else { 190 | action = existingAction; 191 | action.setResult(result, listener); 192 | } 193 | 194 | if (result.getPassCount() == 0 && result.getFailCount() == 0) { 195 | String message = "None of the test reports contained any result."; 196 | if (failOnError) { 197 | throw new AbortException(message); 198 | } else { 199 | logger.error(message); 200 | } 201 | } 202 | 203 | if (existingAction == null) { 204 | build.addAction(action); 205 | } 206 | 207 | if (action.getResult().getFailCount() > 0) { 208 | build.setResult(Result.UNSTABLE); 209 | } 210 | } 211 | 212 | /** 213 | * Collect the test results from the files 214 | * 215 | * @param junitFilePattern the ant file pattern mathing the junit test results file 216 | * @param workspace the build workspace 217 | * @param existingTestResults existing test results to add results to 218 | * @return a junit TestResult 219 | * @throws IOException workspace/jenkins operations may throw 220 | * @throws InterruptedException workspace/jenkins operations may throw 221 | */ 222 | private TestResult getTestResult 223 | (final String junitFilePattern, FilePath workspace, final TestResult existingTestResults) 224 | throws IOException, InterruptedException { 225 | return workspace.act(new MasterToSlaveFileCallable() { 226 | private static final long serialVersionUID = 1L; 227 | 228 | public TestResult invoke(File ws, VirtualChannel channel) throws IOException { 229 | FileSet fs = Util.createFileSet(ws, junitFilePattern); 230 | DirectoryScanner ds = fs.getDirectoryScanner(); 231 | String[] files = ds.getIncludedFiles(); 232 | 233 | if (files.length == 0) { 234 | if (failOnError) { 235 | throw new AbortException(MsTestLogger.format( 236 | "No test report files were found. (Have you specified a pattern matching any file in your workspace ?)")); 237 | } else { 238 | return null; 239 | } 240 | } 241 | if (existingTestResults == null) { 242 | return new TestResult(buildTime, ds, keepLongStdio); 243 | } else { 244 | existingTestResults.parse(buildTime, ds); 245 | return existingTestResults; 246 | } 247 | } 248 | }); 249 | } 250 | 251 | protected Object readResolve() { 252 | if (logLevel == null) { 253 | logLevel = DescriptorImpl.defaultLogLevel; 254 | } 255 | return this; 256 | } 257 | 258 | @Override 259 | public DescriptorImpl getDescriptor() { 260 | return (DescriptorImpl) super.getDescriptor(); 261 | } 262 | 263 | @Extension 264 | @Symbol("mstest") 265 | public static class DescriptorImpl extends BuildStepDescriptor { 266 | 267 | public static final String defaultTestResultsFile = "**/*.trx"; 268 | public static final boolean defaultKeepLongStdio = false; 269 | public static final boolean defaultFailOnError = true; 270 | public static final String defaultLogLevel = "INFO"; 271 | 272 | public DescriptorImpl() { 273 | super(MSTestPublisher.class); 274 | } 275 | 276 | @Override 277 | @NonNull 278 | public String getDisplayName() { 279 | return Messages.MsTest_Publisher_Name(); 280 | } 281 | 282 | @Override 283 | public String getHelpFile() { 284 | return "/plugin/mstest/help.html"; 285 | } 286 | 287 | @Override 288 | public boolean isApplicable(Class jobType) { 289 | return true; 290 | } 291 | } 292 | } 293 | --------------------------------------------------------------------------------