├── .gitignore ├── LocalScripts ├── appcenter_uitest_run.ps1 ├── common.ps1 └── local_uitest_run.ps1 ├── README.md ├── Screenshots ├── pipelines - library - android_demo_var_group.png ├── published artifacts.png ├── run job summary.png ├── run summary - permission grant popup.png ├── run summary - permissions needed.png ├── run_summary.png └── test explorer - filters cleared.png ├── XamarinPipelineDemo.Android ├── Assets │ └── AboutAssets.txt ├── AzureDevOps │ ├── AndroidSetVersion.ps1 │ ├── example.keystore │ ├── example.keystore.README.txt │ └── pipeline-android.yml ├── MainActivity.cs ├── Properties │ ├── AndroidManifest.xml │ └── AssemblyInfo.cs ├── Resources │ ├── AboutResources.txt │ ├── layout │ │ ├── Tabbar.xml │ │ └── Toolbar.xml │ ├── mipmap-anydpi-v26 │ │ ├── icon.xml │ │ └── icon_round.xml │ ├── mipmap-hdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-mdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-xhdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-xxhdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-xxxhdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ └── values │ │ ├── colors.xml │ │ └── styles.xml └── XamarinPipelineDemo.Android.csproj ├── XamarinPipelineDemo.NUnit ├── UnitTest1.cs └── XamarinPipelineDemo.NUnit.csproj ├── XamarinPipelineDemo.UITest ├── AppInitializer.cs ├── Tests.cs └── XamarinPipelineDemo.UITest.csproj ├── XamarinPipelineDemo.sln └── XamarinPipelineDemo ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── MainPage.xaml ├── MainPage.xaml.cs └── XamarinPipelineDemo.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Vim files 5 | *.swp 6 | *.swo 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Xamarin.Android Resource.Designer.cs files 18 | **/*.Android/**/[Rr]esource.[Dd]esigner.cs 19 | **/*.Droid/**/[Rr]esource.[Dd]esigner.cs 20 | **/Android/**/[Rr]esource.[Dd]esigner.cs 21 | **/Droid/**/[Rr]esource.[Dd]esigner.cs 22 | 23 | # Build results 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | x64/ 29 | x86/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | 35 | # Visual Studio 2015 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUNIT 45 | *.VisualState.xml 46 | TestResult.xml 47 | 48 | # Build Results of an ATL Project 49 | [Dd]ebugPS/ 50 | [Rr]eleasePS/ 51 | dlldata.c 52 | 53 | # DNX 54 | project.lock.json 55 | project.fragment.lock.json 56 | artifacts/ 57 | 58 | *_i.c 59 | *_p.c 60 | *_i.h 61 | *.ilk 62 | *.meta 63 | *.obj 64 | *.pch 65 | *.pdb 66 | *.pgc 67 | *.pgd 68 | *.rsp 69 | *.sbr 70 | *.tlb 71 | *.tli 72 | *.tlh 73 | *.tmp 74 | *.tmp_proj 75 | *.log 76 | *.vspscc 77 | *.vssscc 78 | .builds 79 | *.pidb 80 | *.svclog 81 | *.scc 82 | 83 | # Chutzpah Test files 84 | _Chutzpah* 85 | 86 | # Visual C++ cache files 87 | ipch/ 88 | *.aps 89 | *.ncb 90 | *.opendb 91 | *.opensdf 92 | *.sdf 93 | *.cachefile 94 | *.VC.db 95 | *.VC.VC.opendb 96 | 97 | # Visual Studio profiler 98 | *.psess 99 | *.vsp 100 | *.vspx 101 | *.sap 102 | 103 | # TFS 2012 Local Workspace 104 | $tf/ 105 | 106 | # Guidance Automation Toolkit 107 | *.gpState 108 | 109 | # ReSharper is a .NET coding add-in 110 | _ReSharper*/ 111 | *.[Rr]e[Ss]harper 112 | *.DotSettings.user 113 | 114 | # JustCode is a .NET coding add-in 115 | .JustCode 116 | 117 | # TeamCity is a build add-in 118 | _TeamCity* 119 | 120 | # DotCover is a Code Coverage Tool 121 | *.dotCover 122 | 123 | # NCrunch 124 | _NCrunch_* 125 | .*crunch*.local.xml 126 | nCrunchTemp_* 127 | 128 | # MightyMoose 129 | *.mm.* 130 | AutoTest.Net/ 131 | 132 | # Web workbench (sass) 133 | .sass-cache/ 134 | 135 | # Installshield output folder 136 | [Ee]xpress/ 137 | 138 | # DocProject is a documentation generator add-in 139 | DocProject/buildhelp/ 140 | DocProject/Help/*.HxT 141 | DocProject/Help/*.HxC 142 | DocProject/Help/*.hhc 143 | DocProject/Help/*.hhk 144 | DocProject/Help/*.hhp 145 | DocProject/Help/Html2 146 | DocProject/Help/html 147 | 148 | # Click-Once directory 149 | publish/ 150 | 151 | # Publish Web Output 152 | *.[Pp]ublish.xml 153 | *.azurePubxml 154 | # TODO: Comment the next line if you want to checkin your web deploy settings 155 | # but database connection strings (with potential passwords) will be unencrypted 156 | #*.pubxml 157 | *.publishproj 158 | 159 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 160 | # checkin your Azure Web App publish settings, but sensitive information contained 161 | # in these scripts will be unencrypted 162 | PublishScripts/ 163 | 164 | # NuGet Packages 165 | *.nupkg 166 | # The packages folder can be ignored because of Package Restore 167 | **/packages/* 168 | # except build/, which is used as an MSBuild target. 169 | !**/packages/build/ 170 | # Uncomment if necessary however generally it will be regenerated when needed 171 | #!**/packages/repositories.config 172 | # NuGet v3's project.json files produces more ignoreable files 173 | *.nuget.props 174 | *.nuget.targets 175 | 176 | # Microsoft Azure Build Output 177 | csx/ 178 | *.build.csdef 179 | 180 | # Microsoft Azure Emulator 181 | ecf/ 182 | rcf/ 183 | 184 | # Windows Store app package directories and files 185 | AppPackages/ 186 | BundleArtifacts/ 187 | Package.StoreAssociation.xml 188 | _pkginfo.txt 189 | 190 | # Visual Studio cache files 191 | # files ending in .cache can be ignored 192 | *.[Cc]ache 193 | # but keep track of directories ending in .cache 194 | !*.[Cc]ache/ 195 | 196 | # Others 197 | ClientBin/ 198 | ~$* 199 | *~ 200 | *.dbmdl 201 | *.dbproj.schemaview 202 | *.jfm 203 | *.pfx 204 | *.publishsettings 205 | node_modules/ 206 | orleans.codegen.cs 207 | 208 | # Since there are multiple workflows, uncomment next line to ignore bower_components 209 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 210 | #bower_components/ 211 | 212 | # RIA/Silverlight projects 213 | Generated_Code/ 214 | 215 | # Backup & report files from converting an old project file 216 | # to a newer Visual Studio version. Backup files are not needed, 217 | # because we have git ;-) 218 | _UpgradeReport_Files/ 219 | Backup*/ 220 | UpgradeLog*.XML 221 | UpgradeLog*.htm 222 | 223 | # SQL Server files 224 | *.mdf 225 | *.ldf 226 | 227 | # Business Intelligence projects 228 | *.rdl.data 229 | *.bim.layout 230 | *.bim_*.settings 231 | 232 | # Microsoft Fakes 233 | FakesAssemblies/ 234 | 235 | # GhostDoc plugin setting file 236 | *.GhostDoc.xml 237 | 238 | # Node.js Tools for Visual Studio 239 | .ntvs_analysis.dat 240 | 241 | # Visual Studio 6 build log 242 | *.plg 243 | 244 | # Visual Studio 6 workspace options file 245 | *.opt 246 | 247 | # Visual Studio LightSwitch build output 248 | **/*.HTMLClient/GeneratedArtifacts 249 | **/*.DesktopClient/GeneratedArtifacts 250 | **/*.DesktopClient/ModelManifest.xml 251 | **/*.Server/GeneratedArtifacts 252 | **/*.Server/ModelManifest.xml 253 | _Pvt_Extensions 254 | 255 | # Paket dependency manager 256 | .paket/paket.exe 257 | paket-files/ 258 | 259 | # FAKE - F# Make 260 | .fake/ 261 | 262 | # JetBrains Rider 263 | .idea/ 264 | *.sln.iml 265 | 266 | # CodeRush 267 | .cr/ 268 | 269 | # Python Tools for Visual Studio (PTVS) 270 | __pycache__/ 271 | *.pyc 272 | -------------------------------------------------------------------------------- /LocalScripts/appcenter_uitest_run.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Switch] 4 | $SkipBuild, 5 | 6 | [string] 7 | $BuildConfiguration = "Release" 8 | ) 9 | 10 | . .\common.ps1 11 | 12 | if(!$SkipBuild) { 13 | BuildApkAndUiTest 14 | } 15 | 16 | [string] $testOutputDir = ".\artifacts" 17 | [string] $testResultDir = "$testOutputDir\xmls" 18 | [string] $orgName = "JacobEgnerDemos" 19 | 20 | appcenter test run uitest ` 21 | --app "$orgName/$appName" ` 22 | --app-path "$env:UITEST_APK_PATH" ` 23 | --devices "$orgName/demo_device_set" ` 24 | --test-series "master" ` 25 | --locale "en_US" ` 26 | --build-dir "..\$uiTestProjName\bin\$BuildConfiguration" ` 27 | --uitest-tools-dir "..\$uiTestProjName\bin\$BuildConfiguration" ` 28 | --test-output-dir $testOutputDir 29 | 30 | # I'd love to add a `--merge-nunit-xml "AppCenterUiTestResult.xml"` to the 31 | # command, but that gives an error and I have filed an issue: 32 | # https://github.com/microsoft/appcenter-cli/issues/1208 33 | 34 | Expand-Archive "$testOutputDir\nunit_xml_zip.zip" -DestinationPath "$testResultDir" 35 | Write-Output "test result files..." 36 | Get-ChildItem "$testResultDir" 37 | Write-Output "what pipeline task could print to publish the test results..." 38 | Get-ChildItem .\artifacts\xmls\ | ForEach-Object { Write-Output ` 39 | ( "##vso[results.publish " ` 40 | + "runTitle=Android App Center UI Test Run $($_.BaseName);" ` 41 | + "resultFiles=$($_.FullName);" ` 42 | + "type=NUnit;" ` 43 | + "mergeResults=false;" ` 44 | + "publishRunAttachments=true;" ` 45 | + "failTaskOnFailedTests=false;" ` 46 | + "testRunSystem=VSTS - PTR;" ` 47 | + "]" ` 48 | )} 49 | -------------------------------------------------------------------------------- /LocalScripts/common.ps1: -------------------------------------------------------------------------------- 1 | function CmdExists { 2 | param ( 3 | [Parameter(Mandatory)] 4 | [string] 5 | $Cmd 6 | ) 7 | Get-Command $Cmd -ErrorAction SilentlyContinue 8 | } 9 | 10 | function ChooseCmd { 11 | param ( 12 | [Parameter(Mandatory)] 13 | [string[]] 14 | $Cmds 15 | ) 16 | foreach($cmd in $Cmds) { 17 | if(CmdExists($cmd)) { 18 | return $cmd 19 | } 20 | } 21 | return $Cmds[0] 22 | } 23 | 24 | function MsbuildPath { 25 | param ( 26 | [string] $Edition, 27 | [string] $Year = "2019" 28 | ) 29 | "C:\Program Files (x86)\Microsoft Visual Studio\$Year\$Edition\MSBuild\Current\Bin\MSBuild.exe" 30 | } 31 | 32 | function BuildApkAndUiTest { 33 | [string] $msbuild = ChooseCmd(@( 34 | "msbuild", 35 | (MsbuildPath("Enterprise")), 36 | (MsbuildPath("Professional")), 37 | (MsbuildPath("Community")))) 38 | 39 | & $msbuild ../$appName.Android/$appName.Android.csproj ` 40 | /p:Configuration=$BuildConfiguration ` 41 | /t:SignAndroidPackage 42 | & $msbuild ../$uiTestProjName/$uiTestProjName.csproj ` 43 | /p:Configuration=$BuildConfiguration 44 | } 45 | 46 | # END OF FUNCTIONS ############################################################# 47 | 48 | if(!$env:ANDROID_HOME) { 49 | $env:ANDROID_HOME = "C:\Program Files (x86)\Android\android-sdk" 50 | } 51 | 52 | if(!$env:JAVA_HOME) { 53 | $env:JAVA_HOME = (Get-ChildItem 'C:\Program Files\Android\jdk\*jdk*')[0].FullName 54 | } 55 | 56 | [string] $appName = "XamarinPipelineDemo" 57 | [string] $appPackageName = "com.demo.$appName" 58 | [string] $uiTestProjName = "$appName.UITest" 59 | [string] $adb = ChooseCmd(@("adb", "C:\Program Files (x86)\Android\android-sdk\platform-tools\adb.exe")) 60 | 61 | $env:UITEST_APK_PATH = "../$appName.Android/bin/$BuildConfiguration/$appPackageName-Signed.apk" 62 | 63 | -------------------------------------------------------------------------------- /LocalScripts/local_uitest_run.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Switch] 4 | $SkipBuild, 5 | 6 | [string] 7 | $BuildConfiguration = "Release" 8 | ) 9 | 10 | . .\common.ps1 11 | 12 | if(!$SkipBuild) { 13 | BuildApkAndUiTest 14 | } 15 | 16 | & $adb uninstall $appPackageName 17 | & $adb uninstall "$appPackageName.test" 18 | 19 | $nunitConsole = ChooseCmd(@( 20 | "nunit3-console", 21 | "C:\Program Files (x86)\NUnit.org\nunit-console\nunit3-console.exe")) 22 | # you'll need a connected local Android device or Android emulator running 23 | & $nunitConsole ` 24 | ../$uiTestProjName/bin/$BuildConfiguration/$appName.UITest.dll ` 25 | --output=uitest.log 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Table Of Contents # 2 | * [Introduction](#Introduction) 3 | * [Notable Files](#Notable-Files) 4 | * [Getting Started On Local Machine](#Getting-Started-On-Local-Machine) 5 | * [Getting Started On Azure DevOps](#Getting-Started-On-Azure-DevOps) 6 | * [Explanation Of The Journey: Deadends, Pitfalls, Solutions](#Explanation-Of-The-Journey-Deadends-Pitfalls-Solutions) 7 | * [Must Use MacOS Agent](#Must-Use-MacOS-Agent) 8 | * [MacOS Agent Pitfalls](#MacOS-Agent-Pitfalls) 9 | * [MacOS Agent Directories](#MacOS-Agent-Directories) 10 | * [Fresh Autogenerated Pipeline](#Fresh-Autogenerated-Pipeline) 11 | * [General Pipeline YAML Advice](#General-Pipeline-YAML-Advice) 12 | * [Pipeline Variables](#Pipeline-Variables) 13 | * [Pipeline Triggers](#Pipeline-Triggers) 14 | * [Pipeline Scripts And Strings](#Pipeline-Scripts-And-Strings) 15 | * [Give Each Build An Increasing Android App Version](#Give-Each-Build-An-Increasing-Android-App-Version) 16 | * [How To Choose The Version](#How-To-Choose-The-Version) 17 | * [Build The APK File](#Build-The-APK-File) 18 | * [Sign The APK File](#Sign-The-APK-File) 19 | * [Keystore Background](#Keystore-Background) 20 | * [AndroidSigning Task](#AndroidSigning-Task) 21 | * [Publish The APK Files As Build Artifacts](#Publish-The-APK-Files-As-Build-Artifacts) 22 | * [Build And Run Unit Tests](#Build-And-Run-Unit-Tests) 23 | * [UI Tests In Azure DevOps](#UI-Tests-In-Azure-DevOps) 24 | * [Set Up And Start Android Emulator](#Set-Up-And-Start-Android-Emulator) 25 | * [Build UI Tests](#Build-UI-Tests) 26 | * [Run Emulator UI Tests](#Run-Emulator-UI-Tests) 27 | * [Publish Emulator UI Tests](#Publish-Emulator-UI-Tests) 28 | * [UI Tests In App Center](#UI-Tests-In-App-Center) 29 | * [Set Up App Center](#Set-Up-App-Center) 30 | * [Experiment With `appcenter` CLI](#Experiment-With-appcenter-CLI) 31 | * [Run App Center UI Tests](#Run-App-Center-UI-Tests) 32 | * [Publish App Center UI Tests In Azure DevOps](#Publish-App-Center-UI-Tests-In-Azure-DevOps) 33 | * [Problems Accessing Stuff](#Problems-Accessing-Stuff) 34 | * [Thanks To Those Who Helped Me](#Thanks-To-Those-Who-Helped-Me) 35 | 36 | # Introduction # 37 | I'm making this demo repo and writeup because it was surprisingly and 38 | frustratingly difficult to get Xamarin.UITest tests for Android to run on a 39 | Microsoft-hosted agent in an Azure DevOps pipeline. NO App Center. NO 40 | self-hosted agents. I just wanted to do everything in Azure DevOps. 41 | 42 | (Once I got that to work, I did add in App Center UI testing...which was also 43 | surprisingly difficult, so hopefully this demo is helpful for that as well.) 44 | 45 | This demo has grown into showing how to accomplish quite a few common goals for 46 | an Azure Devops continuous integration pipeline for the Android portion of a 47 | Xamarin app... 48 | * Each build gets its own `versionCode` and `versionName`. 49 | * Build the APK. 50 | * Sign the APK. 51 | * Publish the APK as a pipeline artifact. 52 | * Do unit tests (NUnit). 53 | * Do UI tests (Xamarin.UITest) in Azure DevOps, which involves several Android 54 | emulator steps. 55 | * Do UI tests in App Center. 56 | * Publish all test results (including device-labeled App Center test results in 57 | Azure DevOps test explorer). 58 | 59 | This demo is not about getting started on unit testing or UI testing; the demo 60 | is about getting these things to work in an Azure DevOps pipeline. 61 | 62 | You can see a 63 | [successful run](https://jmegner.visualstudio.com/Demos/_build/results?buildId=59&view=results), 64 | a 65 | [successful job overview](https://jmegner.visualstudio.com/Demos/_build/results?buildId=59&view=logs), 66 | [published artifacts](https://jmegner.visualstudio.com/Demos/_build/results?buildId=59&view=artifacts&pathAsName=false&type=publishedArtifacts), 67 | and 68 | [unit+UI test results](https://jmegner.visualstudio.com/Demos/_build/results?buildId=59&view=ms.vss-test-web.build-test-results-tab). 69 | 70 | This repo is available as a 71 | [visualstudio.com repo](https://jmegner.visualstudio.com/Demos/_git/XamarinPipelineDemo) 72 | and a 73 | [github repo](https://github.com/jmegner/XamarinPipelineDemo). 74 | As of 2020-Dec-24, Azure DevOps offers a 75 | [free tier](https://azure.microsoft.com/en-us/pricing/details/devops/azure-devops-services/) 76 | with 30 build hours per month and 2 GiB of artifact storage. The free tier was 77 | more than enough for all the pipeline needs of this demo. 78 | 79 | This writeup is available as a 80 | [github readme](https://github.com/jmegner/XamarinPipelineDemo/blob/main/README.md), 81 | [visualstudio.com readme](https://jmegner.visualstudio.com/Demos/_git/XamarinPipelineDemo?path=%2FREADME.md&version=GBmain&_a=preview), 82 | and 83 | [blog post](https://jacobegner.blogspot.com/2020/12/xamarin-pipeline-demo.html). 84 | The repo readmes will be kept up to date, but the blog post may not receive 85 | many updates after 2020-12-24. Readme section links are oriented for GitHub. 86 | 87 | # Notable Files # 88 | The 89 | [`XamarinPipelineDemo.Android/AzureDevOps/`](XamarinPipelineDemo.Android/AzureDevOps/) 90 | folder has most of the notable files... 91 | * [`pipeline-android.yml`](XamarinPipelineDemo.Android/AzureDevOps/pipeline-android.yml): 92 | the pipeline definition and heart of this demo. 93 | * [`AndroidSetVersion.ps1`](XamarinPipelineDemo.Android/AzureDevOps/AndroidSetVersion.ps1): 94 | the script that manipulates the Android manifest file to update the 95 | `versionName` and `versionCode` attributes. 96 | * [`example.keystore`](XamarinPipelineDemo.Android/AzureDevOps/example.keystore): 97 | for signing the APK. Normally keystore files are sensitive and you wouldn't 98 | put them (and their passwords) in your repo, but this is a demo. 99 | 100 | [`XamarinPipelineDemo.UITest/AppInitializer.cs`](XamarinPipelineDemo.UITest/AppInitializer.cs): 101 | the autogenerated `AppInitializer.cs` has been modified so that you can specify 102 | which APK file to install for testing, or which keystore to match an already 103 | installed (signed) APK. I suggest the APK file methodology. 104 | 105 | [`local_uitest_run.ps1`](LocalScripts/local_uitest_run.ps1): 106 | script to run UITest tests (on a local Android device or emulator) in way most 107 | similar to how the pipeline will do it. 108 | 109 | [`appcenter_uitest_run.ps1`](LocalScripts/appcenter_uitest_run.ps1): 110 | script to run UITest tests remotely via App Center. You'll need to set up your 111 | own App Center account (including app and device set) and modify the script to 112 | use that account. 113 | 114 | [`Screenshots`](Screenshots) 115 | folder has some screenshots of the results of a 116 | working pipeline run, and some of the web interface you need to tangle with to 117 | get the pipeline working. 118 | 119 | # Getting Started On Local Machine # 120 | First, check that it works on your machine. Open the solution in Visual Studio 121 | 2019, and deploy the Release build to an Android emulator or connected Android 122 | device (just select Release build configuration and launch the debugger). The 123 | app should show you a page with a label that says "Some text.". 124 | 125 | In Visual Studio's test explorer, run both the Nunit and UITest tests. 126 | Everything should pass. 127 | 128 | Also, to run the UITest tests in the way most similar to how the pipeline will 129 | do it, install a recent stable 130 | [`nunit3-console` release](https://github.com/nunit/nunit-console/releases), 131 | go into the `LocalScripts` folder and run `local_uitest_run.ps1`. You'll 132 | get a test results file `TestResult.xml` and a detailed log `uitest.log` that 133 | is useful for troubleshooting. The script tries to use `adb` and `msbuild` from 134 | your `PATH` environment variable and a few other locations. You might have to 135 | add your `adb` or `msbuild` directories to your `PATH`. Also, you might have to 136 | set the `ANDROID_HOME` environment variable to something like `C:\Program Files 137 | (x86)\Android\android-sdk` and the `JAVA_HOME` environment variable to 138 | something like `C:\Program Files\Android\jdk\microsoft_dist_openjdk_1.8.0.25` 139 | 140 | # Getting Started On Azure DevOps # 141 | In the Pipelines => Library section of your Azure DevOps project, you need to 142 | do a few things. 143 | 144 | You need to set up the keystore file and variables... 145 | * Upload `example.keystore` as a secure file. 146 | * Create a variable group named `android_demo_var_group`. In it, 147 | create the following variables... 148 | * `androidKeystoreSecureFileName: example.keystore` 149 | * `androidKeyAlias: androiddebugkey` 150 | * `androidKeystorePassword: android` 151 | * `androidKeyPassword: android` 152 | * Make the `androidKeystorePassword` and `androidKeyPassword` secret by clicking the padlock icon. 153 | 154 | You need to create a pipeline from the yaml pipeline definition file... 155 | * Upload the repo to Azure DevOps. 156 | * Create a new pipeline. 157 | * When asked "where is your code?", choose "Azure Repos Git". 158 | * Select the XamarinPipelineDemo repo. 159 | * Select "Existing Azure Pipelines YAML file". 160 | * Select the `XamarinPipelineDemo/XamarinPipelineDemo.Android/AzureDevOps/pipeline-android.yml` 161 | as the existing yaml file. 162 | 163 | Run the pipeline and you'll have to click a few things to give the pipeline 164 | access to these secure files and secret variables. To grant permission to the 165 | pipeline, you might have to go to the run summary page (see 166 | [`Screenshots`](Screenshots) 167 | folder). 168 | 169 | # Explanation Of The Journey: Deadends, Pitfalls, Solutions # 170 | Note that things may change after this demo was made (2020-12-19). Some 171 | limitations may go away, and some workaround no longer needed. I'd love to 172 | hear about them if you ever encounter a way this demo should be updated. 173 | 174 | ## Must Use MacOS Agent ## 175 | First of all, doing Xamarin.UITest tests on Microsoft-hosted agent in an Azure 176 | DevOps pipeline has some important constraints. Microsoft-hosted agents for 177 | Window and Linux run in a virtual machine that can not run the Android 178 | emulator, so only the MacOS agent can run the Android emulator ([MS docs 179 | page](https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/android?view=azure-devops&viewFallbackFrom=vsts#test-on-the-android-emulator)). 180 | 181 | With a self-hosted agent that is not a virtual machine, you can use any of the 182 | three OSes. With App Center, the tests are run on a real device, and there is 183 | no need to run the Android emulator, so again, you can use any of the three 184 | OSes. 185 | 186 | ### MacOS Agent Pitfalls ### 187 | The MacOS agent has a few pitfalls to watch out for. 188 | 189 | * MacOS must use Mono when dealing with .NET Framework stuff (originally made 190 | just for Windows). So, .NET Framework stuff that works on your Windows 191 | machine may not do so well in the pipeline. 192 | * Try to make your project target .NET Core or .NET 5 where possible, 193 | especially your unit test project. 194 | * You can't use DotNetCoreCLI task on a MacOS agent to run 195 | test projects that target .NET Framework. 196 | [Mono's open issue 6984](https://github.com/mono/mono/issues/6984) 197 | says that you can do "dotnet build" on a .NET Framework project, but you 198 | can't "dotnet test". 199 | * Xamarin.UITest MUST be .NET Framework, so you can not use DotNetCoreCLI task 200 | to run Xamarin.UITest tests. 201 | * MacOS agent also doesn't support 202 | [`VSTest`](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/vstest?view=azure-devops) 203 | or 204 | [`VsBuild`](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/visual-studio-build?view=azure-devops) 205 | tasks. 206 | * The only thing left to do for Xamarin.UITest is a 207 | [`MSBuild` task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/msbuild?view=azure-devops) 208 | to build it, then directly run `nunit3-console` to run the Xamarin.UITest 209 | tests. 210 | * MacOS agents are case sensitive for path stuff while Windows is not, so make 211 | sure your pipeline stuff is case-appropriate. 212 | * On Windows, you might be used to using Unix-inspired PowerShell aliases like 213 | "ls" and "mv". Do not use those aliases. In MacOS, even inside a PowerShell 214 | script, commands like "ls" will invoke the Unix command instead of the 215 | PowerShell cmdlet. 216 | 217 | ### MacOS Agent Directories ### 218 | During pipeline execution, there are three major directories to think about: 219 | * `Build.SourcesDirectory`, which is often `/Users/runner/work/1/s/`. 220 | * `Build.BinariesDirectory`, which is often `/Users/runner/work/1/b/`. 221 | * `Build.ArtifactStagingDirectory`, which is often `/Users/runner/work/1/a/`. 222 | 223 | The repo for your pipeline is automatically put in the 224 | `Build.SourcesDirectory`. The other two directories are just natural places 225 | for you to put stuff. For instance, build outputs to `Build.BinariesDirectory` 226 | and special files you want to download later (artifacts) to 227 | `Build.ArtifactStagingDirectory`. The 228 | [`PublishBuildArtifacts`](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/publish-build-artifacts?view=azure-devops) 229 | task even defaults to publishing everything in the 230 | `Build.ArtifactStagingDirectory`. 231 | 232 | ## Fresh Autogenerated Pipeline ## 233 | If you make a new pipeline for a Xamarin Android app, you get an autogenerated 234 | yaml file that... 235 | * Triggers on your main or master branch. 236 | * Selects desired agent type (the `pool` section `vmImage` value). 237 | * Sets `buildConfiguration` and `outputDirectory` variables. 238 | * Does usual nuget stuff so you download the nuget packages used by your 239 | solutions. 240 | * The XamarinAndroid task builds all "`*droid*.csproj`" projects (probably just 241 | one for you), generating an unsigned APK file. 242 | 243 | That's it. You can't even access the unsigned APK file after the pipeline 244 | runs; you just get to know whether the agent was able to make the unsigned APK. 245 | 246 | I'll explain how and why we add to the pipeline to accomplish the goals I 247 | mentioned in the introduction. 248 | 249 | ## General Pipeline YAML Advice ## 250 | You're going to have to learn nuances of yaml. If you don't already know yaml 251 | and the unique quirks of pipeline yaml, it's going to trip you up somewhere. 252 | 253 | ### Pipeline Variables ### 254 | One of the first learning hurdles for dealing with pipeline is learning enough 255 | to use variables effectively. 256 | 257 | The variable section in a fresh autogenerated pipeline looks like this... 258 | ``` 259 | variables: 260 | name1: value1 261 | name2: value2 262 | ``` 263 | ...which is nice and compact. But if you need to use a variable group, 264 | [you have to](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups?view=azure-devops&tabs=yaml#use-a-variable-group) 265 | go with the more verbose way... 266 | ``` 267 | variables: 268 | - group: nameOfVariableGroup 269 | - name: name1 270 | value: value1 271 | - name: name2 272 | value: value2 273 | ``` 274 | 275 | I still haven't read the entire 276 | [MS Docs page on pipeline variables](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch) 277 | because it is so long. Unfortunately there are 278 | [three different syntaxes](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#understand-variable-syntax) 279 | for referencing variables. You can mostly use 280 | [macro syntax](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#macro-syntax-variables), 281 | which looks like `$(someVariable)` and leads to the variable being processed 282 | just before a task executes. Macro syntax can not be used in `trigger` or 283 | `resource` sections, and can not be used as yaml keys. 284 | 285 | If the pipeline encounters `$(someVariable)` and doesn't recognize 286 | `someVariable` as a variable, then the expression stays as is (because maybe 287 | it'll be usable by PowerShell or whatever you're executing). 288 | 289 | So, if you get errors that directly talk about `$(someVariable)` rather than 290 | the value of `someVariable`, then `someVariable` isn't defined. You need to 291 | check your spelling, and if it's a variable from a variable group (defined in 292 | Library section of web interface), you need to explicitly reference the 293 | variable group in your `variables` section. 294 | 295 | My pipeline yaml mostly uses macro syntax. One notable exception is 296 | [runtime expression syntax](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#runtime-expression-syntax) 297 | (`$[variables.someVariable]`) in conditions and expressions, as is 298 | [recommended](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#what-syntax-should-i-use). 299 | You can see the runtime expression syntax in my pipeline's step conditions, 300 | just search for "condition:" or "variables.". Another exception is Azure 301 | DevOps's surprising (but reasonable) way of 302 | [setting/creating pipeline variables from scripts](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-variables-in-scripts): 303 | outputting a line to standard output that conforms to 304 | [logging command syntax](https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash); 305 | here's an example: 306 | ``` 307 | - pwsh: Write-Output "##vso[task.setvariable variable=someVariable]some string with spaces allowed" 308 | ``` 309 | 310 | Non-secret variables are 311 | [mapped to environment variables](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#access-variables-through-the-environment) 312 | for each task. 313 | 314 | ### Pipeline Triggers ### 315 | A freshly autogenerated pipeline might have a 316 | [trigger](https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/azure-repos-git?view=azure-devops&tabs=yaml#ci-triggers) 317 | section... 318 | ``` 319 | trigger: 320 | - main 321 | ``` 322 | ...which will make the pipeline trigger for every change to the main branch. 323 | But if you have multiple target platforms (android, iOS, uwp), each having 324 | their own pipeline, then you get a lot of unnecessary builds when you update 325 | something only relevant to one platform. 326 | 327 | So, you probably want a 328 | [path-based trigger](https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/azure-repos-git?view=azure-devops&tabs=yaml#paths). 329 | Note that wildcards are unsupported and all paths are relative to the root of 330 | the repo. Here's a trigger section for a hypothetical android pipeline... 331 | ``` 332 | trigger: 333 | branches: 334 | include: 335 | - main 336 | paths: 337 | include: 338 | # common 339 | - 'MyApp' 340 | - 'MyApp.NUnit' 341 | - 'MyApp.UITest' 342 | - 'Util' 343 | - 'XamarinUtil' 344 | - 'MyApp.sln' 345 | # platform 346 | - 'MyApp.Android' 347 | ``` 348 | 349 | Also, this path-based trigger stuff is why this demo's android pipeline yml 350 | file and android version script are under 351 | [`XamarinPipelineDemo.Android/AzureDevOps`](XamarinPipelineDemo.Android/AzureDevOps) 352 | rather than under a root-level AzureDevOps folder. A change to these 353 | android-pipeline-specific file should only trigger an android pipeline build, 354 | and putting them under an android folder makes that easy trigger-wise. 355 | 356 | Similarly, 357 | [`local_uitest_run.ps1`](LocalScripts/local_uitest_run.ps1) is in a 358 | `LocalScripts` folder instead of the XamarinPipelineDemo.UITest folder because 359 | changes to a local-use-only script should not trigger a pipeline build. There 360 | is also the option of having a `XamarinPipelineDemo.UITest/LocalScripts` folder 361 | and listing that folder in the yaml's trigger-paths-exclude list. 362 | 363 | ### Pipeline Tasks ### 364 | Some tasks support path wildcards in their inputs, some don't. Always check the 365 | [task reference](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/?view=azure-devops) 366 | before using path wildcards. If you get an error message like "not found 367 | PathToPublish: /User/runner/work/1/a/\*.apk", the fact that the path it 368 | couldn't find has a wildcard should make you double check whether wildcards are 369 | supported for that task input. 370 | 371 | Sometimes the task is a wrapper around some tool, and the task's documentation 372 | doesn't go into much detail into the behavior of the tool. For instance, 373 | [AndroidSigning](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/android-signing?view=azure-devops) 374 | is a wrapper around 375 | [`apksigner`](https://developer.android.com/studio/command-line/apksigner), 376 | and you have to get all the way down to the `--out` option section of the 377 | `apksigner` doc to learn that the _absence_ of the option leads to the APK file 378 | being signed in place, overwriting the input APK. 379 | 380 | Sometimes looking at the 381 | [Azure pipeline tasks source code](https://github.com/microsoft/azure-pipelines-tasks/tree/master/Tasks) 382 | is useful. 383 | 384 | ### Pipeline Scripts And Strings ### 385 | In your pipeline, you might want to do something simple, like copy some files. 386 | Sometimes there is a task for what you want to do, like 387 | [CopyFiles](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/copy-files?view=azure-devops&tabs=yaml), 388 | but often there isn't. A good way to accomplish these small things is to use one of the script tasks... 389 | 390 | * [Bash](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/bash?view=azure-devops): 391 | runs on MacOS, Linux, and Windows. 392 | * [BatchScript](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/batch-script?view=azure-devops): 393 | runs on Windows. 394 | * [CmdLine](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/command-line?view=azure-devops&tabs=yaml): 395 | uses bash on Linux and MacOS; uses cmd.exe on Windows. 396 | * [PowerShell](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/powershell?view=azure-devops): 397 | runs on MacOS, Linux, and Windows. 398 | 399 | I prefer PowerShell because... 400 | * It runs on all the agents in the same way. 401 | * It will run on people's local machines. It comes preinstalled in Windows and 402 | I think it's easy enough to install on Linux and MacOS. 403 | * I think it's the most capable of the languages. I think PowerShell helps 404 | keep simple tasks easy and can use anything in the .net ecosystem, like 405 | `System.Collections.Generic.Dictionary`, which is especially nice for 406 | Xamarin developers. 407 | 408 | In fact, I learned PowerShell because of dealing with Xamarin pipelines, and 409 | PowerShell is now my go-to language for quick Windows scripts. 410 | 411 | There are a few ways to do scripts in pipelines, but first you should 412 | understand yaml multi-line strings. The `>` character causes the following 413 | indented block to be treated "folded style": as a single string with no line 414 | breaks (except one at the end). The `|` character causes the following 415 | indented block to be treated "literal style": as a single string with line 416 | breaks preserved. Good explanation of mult-line strings at 417 | [this stackoverflow answer](https://stackoverflow.com/a/21699210) 418 | and 419 | [yaml-multiline.info](https://yaml-multiline.info/). 420 | 421 | Here are some script examples... 422 | ``` 423 | - pwsh: SomeCommand | CommandReceivingPipedStuffFromPreviousCommand; SomeSeparateCommand 424 | displayName: 'some inline one-liner script' 425 | 426 | - pwsh: | 427 | SomeCommand | CommandReceivingPipedStuffFromPreviousCommand 428 | SomeSeparateCommand 429 | displayName: 'some inline multi-liner script' 430 | 431 | - task: PowerShell@2 432 | displayName: 'calling a PowerShell script file in the repo' 433 | inputs: 434 | filePath: '$(theScriptDir)/SomeScript.ps1' 435 | # '>' used so we can have multiple lines treated as one line 436 | arguments: > 437 | -SomeScriptArg "SomeValueInQuotes" 438 | -AnotherScriptArg AnotherValueShowingQuotesNotAlwaysNeeded 439 | ``` 440 | 441 | Note how `|` characters can appear in the scripts; that's totally fine. 442 | 443 | ## Give Each Build An Increasing Android App Version ## 444 | If the Azure DevOps pipeline is going to be making the APKs we'll be releasing, 445 | we need unique `versionCode` and `versionName` values for each build. 446 | 447 | [Reminder](https://developer.android.com/studio/publish/versioning#appversioning): 448 | `versionCode` is a positive integer that is used by Android to compare versions 449 | and is not shown to the user. `versionName` is text displayed to the user and 450 | that is its only use. 451 | 452 | Short version: The 'Set build-based Android app version' task uses the YAML 453 | counter function on the pipeline name (`Build.DefinitionName`) to set the 454 | `versionCode` and the `Build.BuildNumber` to set the `versionName`. This task 455 | is executed right before the XamarinAndroid build task and calls a PowerShell 456 | script to modify the Android manifest file. 457 | 458 | ### How To Set The Android App Version ### 459 | James Montemagno's and Andrew Hoefling's "Mobile App Tasks for VSTS" 460 | ([Azure DevOps plugin](https://marketplace.visualstudio.com/items?itemName=vs-publisher-473885.motz-mobile-buildtasks), 461 | [Github repo](https://github.com/jamesmontemagno/vsts-mobile-tasks)) 462 | has an `AndroidBumpVersion` task that does half of the job: setting the 463 | `versionCode` and `versionName`. 464 | 465 | Some people are not allowed to use Azure DevOps plugins (perhaps for security 466 | by their employer), so we will not use this as a plugin. Azure DevOps plugins 467 | are run via a Node server, so the plugin would use 468 | [`tasks/AndroidBumpVersion/task.ts`](https://github.com/jamesmontemagno/vsts-mobile-tasks/blob/master/tasks/AndroidBumpVersion/task.ts), 469 | but thankfully James has also provided PowerShell and bash equivalents of his 470 | plugin tasks, so you can look at those files. 471 | 472 | I went with his PowerShell script, fixed a bug, and cleaned it up 473 | ([pull request 39](https://github.com/jamesmontemagno/vsts-mobile-tasks/pull/39), 474 | [current code](https://github.com/jamesmontemagno/vsts-mobile-tasks/blob/master/tasks/AndroidBumpVersion/task.ps1)). 475 | The result is this demo's 476 | [AndroidSetVersion.ps1](XamarinPipelineDemo.Android/AzureDevOps/AndroidSetVersion.ps1). 477 | 478 | (Note: recent versions of PowerShell are cross platform, so you can run 479 | PowerShell on MacOS and Linux. But again, be mindful of Unix commands 480 | overriding PowerShell aliases and you can't be case-insensitive.) 481 | 482 | The essence of the script is that the Android manifest file is XML and inside 483 | the `manifest` element, set the `android:versionCode` and 484 | `android:versionName` attributes appropriately. Thankfully PowerShell has the 485 | [XmlDocument](https://docs.microsoft.com/en-us/dotnet/api/system.xml.xmldocument?view=net-5.0) 486 | class and the 487 | [Select-XML](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/select-xml?view=powershell-7.1#outputs) 488 | cmdlet that gives you easy-to-manipulate 489 | [SelectXmlInfo](https://docs.microsoft.com/en-us/dotnet/api/microsoft.powershell.commands.selectxmlinfo?view=powershellsdk-7.0.0) 490 | objects. 491 | 492 | ### How To Choose The Version ### 493 | The second half of the problem is how to have an increasing and meaningful 494 | `versionCode` and `versionName`. Azure DevOps pipelines will have [pre-defined 495 | variables](https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml), 496 | including... 497 | * `Build.BuildId`: a positive integer that is build id that is unique across 498 | your 499 | [organization](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/organization-management?view=azure-devops) 500 | and will appear in the build's URL (ex: 501 | `dev.azure.com/SomeOrganization/SomeProject/_build/results?buildId=123456`). 502 | * `Build.BuildNumber`: a string (not a number, especially if you set the 503 | [`name` variable](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/run-number?view=azure-devops&tabs=yaml). 504 | The [default format](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/run-number?view=azure-devops&tabs=yaml#tokens) 505 | is "`$(Date:yyyyMMdd)$(Rev:.r)`", which looks like "20201231.7" and is unique 506 | only within the pipeline. 507 | * `Build.DefinitionName`: the name of the pipeline. 508 | 509 | I think that the default `Build.BuildNumber` makes sense for `versionName`; 510 | it's unique, increasing, and easy for you to lookup the build/commit for the 511 | version name a user sees. I don't like `Build.BuildId` for `versionCode` 512 | because consecutive builds will probably not have consecutive `versionCode` 513 | values because of all the other builds in your Azure DevOps organization. 514 | `Build.BuildId` is probably just going to be a large, meaningless number for 515 | you. 516 | 517 | Thankfully, Andrew Hoefling wrote “[Azure Pipelines Custom Build Numbers in 518 | YAML 519 | Templates](https://www.andrewhoefling.com/Blog/Post/azure-pipelines-custom-build-numbers-in-yaml-templates)”, 520 | which shows how you can get a simple {1,2,3,...} progression for a build using 521 | the yaml [counter 522 | function](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/expressions?view=azure-devops#counter). 523 | MS docs on defining pipeline variables has a [counter 524 | example](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-variables-by-using-expressions) 525 | too. 526 | 527 | Here's a snippet that shows a simple `pipelineBuildNumber` that goes up 528 | {0,1,2,...} and a `versionRevision` that counts up but gets reset everytime you 529 | change the `versionMajorMinor` value. 530 | ``` 531 | variables: 532 | # for doing Major.Minor.Revision; 533 | # any time you change versionMajorMinor, 534 | # versionRevision uses a new counter 535 | - name: 'versionMajorMinor' 536 | value: '0.0' 537 | - name: 'versionRevision' 538 | value: $[counter(variables['versionMajorMinor'], 0)] 539 | # for doing simple pipeline build counter 540 | - name: 'pipelineBuildNumber' 541 | value: $[counter(variables['Build.DefinitionName'], 1)] 542 | ``` 543 | 544 | ## Build The APK File ## 545 | Thankfully autogenerated android pipelines and internet examples give you a 546 | [XamarinAndroid](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/xamarin-android?view=azure-devops) 547 | step that can build the apk for you. Here's the demo's step for that... 548 | ``` 549 | - task: XamarinAndroid@1 550 | inputs: 551 | projectFile: '**/*droid*.csproj' 552 | outputDir: '$(outputDir)' 553 | configuration: '$(buildConfiguration)' 554 | ``` 555 | 556 | One confusing thing though is some places will say to use the 557 | [Gradle task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/gradle?view=azure-devops) 558 | instead of the deprecated 559 | [Android build task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/android-build?view=azure-devops). 560 | I am 90% sure Gradle is for native Android apps, not Xamarin. I do know that 561 | I've never had to use anything Gradle-related for my Xamarin stuff and 562 | XamarinAndroid seems fine. 563 | 564 | ## Sign The APK File ## 565 | 566 | ### Keystore Background ### 567 | You'll want to sign the APK file so it can be installed on users' devices and 568 | distributed on Google Play. This repo already comes with a keystore file 569 | (remember: don't put your keystore in your repo; it should be more tightly 570 | controlled and uploaded as a secure file to Azure DevOps), but you can create 571 | your own keystore by following these 572 | [MS Docs instructions](https://docs.microsoft.com/en-us/xamarin/android/deploy-test/signing/) 573 | (don't do the "Sign the APK" section). 574 | 575 | You might get confused that if you make a keystore in Visual Studio, you have 576 | to choose a "keystore password", but not a "key password", and lots of other 577 | places talk about the "key password". The 578 | "[key and certificate options](https://developer.android.com/studio/command-line/apksigner#options-sign-key-cert)" 579 | section of the `apksigner` doc might help you understand. A keystore can contain 580 | multiple keys, each identified by a key alias. The keystore itself 581 | password-protected, and each key might have its own password. This 582 | [keytool example](https://docs.microsoft.com/en-us/xamarin/android/deploy-test/signing/manually-signing-the-apk) 583 | makes me think a common behavior is for a key password to default to the same 584 | as the keystore password. 585 | 586 | One approach that has worked for me so far: when you are asked for a key 587 | password, and you don't recall there being a key password, you can probably put 588 | the keystore password. 589 | 590 | Another confusion you may have is that the 591 | [`AndroidSigning`](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/android-signing?view=azure-devops) 592 | task has an input named `keystoreAlias` (also called `apksignerKeystoreAlias`), 593 | but keystores do not have aliases; keys within keystores have aliases. You 594 | specify the keystore by the file name, then you specify the key by the key's 595 | alias. I have reported this misnaming as a 596 | [problem on Developer Community](https://developercommunity.visualstudio.com/content/problem/1292515/androidsigning.html). 597 | 598 | ### AndroidSigning Task ### 599 | This is the demo's 600 | [AndroidSigning](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/android-signing?view=azure-devops) 601 | task (and required reference to appropriate variable group)... 602 | ``` 603 | variables: 604 | - group: android_demo_var_group 605 | 606 | ... 607 | 608 | - task: AndroidSigning@3 609 | displayName: 'sign APK with example keystore' 610 | inputs: 611 | apkFiles: '$(outputDir)/*.apk' 612 | apksignerKeystoreFile: '$(androidKeystoreSecureFileName)' 613 | apksignerKeystoreAlias: '$(androidKeyAlias)' 614 | apksignerKeystorePassword: '$(androidKeystorePassword)' 615 | apksignerArguments: '--verbose --out $(finalApkPathSigned)' 616 | ``` 617 | 618 | Remember to follow the steps from 619 | [Getting Started On Azure DevOps](#getting-started-on-azure-devops) 620 | for uploading the keystore as secure file and creating the needed variable 621 | group with needed variables. 622 | 623 | The task doc says it accepts wildcards for `apkFiles`. (Remember, don't assume 624 | tasks accept wildcards, check the task doc). Also, the doc states that the 625 | referenced keystore file _must_ be a secure file, which should be fine for you. 626 | However, if you want to get around this restriction, you could use a 627 | [PowerShell task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/powershell?view=azure-devops) 628 | to call apksigner directly. 629 | 630 | Here is the error message if you try to use something other than a secure file 631 | for your keystore: 632 | ``` 633 | There was a resource authorization issue: "The pipeline is not valid. Job Job: Step AndroidSigning2 input keystoreFile references secure file /path/to/nonsecure/file which could not be found. The secure file does not exist or has not been authorized for use. For authorization details, refer to https://aka.ms/yamlauthz." 634 | ``` 635 | 636 | If you get errors like "can't find $(someVariable) secure file", that means the 637 | `someVariable` is not defined. Check that you are referencing the appropriate 638 | variable group in your yaml's `variables` section, and check that 639 | `someVariable` exactly matches what you have in your variable group. 640 | 641 | By default, apksigner overwrites the APK file, and therefore the AndroidSigning 642 | task overwrites the APK file, which could be fine for you. But I wanted the 643 | signed APK to go into the artifact staging directory (path held in predefined 644 | variable `Build.ArtifactStagingDirectory`) with a particular file name (not the 645 | default `com.demo.XamarinPipelineDemo.apk`), so I used 646 | [apksigner's `--out` argument](https://developer.android.com/studio/command-line/apksigner#options-sign-general). 647 | 648 | Note that `finalApkPathSigned` puts the `Build.BuildNumber` and 649 | `pipelineBuildNumber` in the file name. 650 | 651 | If you ever want to double check whether an APK has been signed, and by which 652 | keystore, use apksigner (possibly at `C:\Program Files 653 | (x86)\Android\android-sdk\build-tools\SOME_VERSION\apksigner.bat`). Do 654 | `apksigner verify --print-certs THE_APK_PATH` and the first line tells you 655 | about the key that signed the APK or `DOES NOT VERIFY` if not signed. 656 | 657 | Likewise, for looking at keystore files, you can use keytool (possibly at 658 | `C:\Program 659 | Files\Android\jdk\microsoft_dist_openjdk_1.8.0.25\bin\keytool.exe`). `keytool 660 | -v -list -keystore KEYSTORE_PATH` will tell you about keys in the keystore, 661 | even if you provide no keystore password. 662 | 663 | ## Publish The APK Files As Build Artifacts ## 664 | I wanted to have both unsigned and signed APKs as build artifact. 665 | 666 | Here is the 667 | [MS Doc description](https://docs.microsoft.com/en-us/azure/devops/pipelines/artifacts/artifacts-overview?view=azure-devops) 668 | of build artifacts and pipeline artifacts: 669 | 670 | > Build artifacts are the files that you want your build to produce. Build 671 | > artifacts can be nearly anything that your team needs to test or deploy your 672 | > app. For example, you've got .dll and .exe executable files and a .PDB 673 | > symbols file of a .NET or C++ Windows app. 674 | > 675 | > You can use pipeline artifacts to help store build outputs and move 676 | > intermediate files between jobs in your pipeline. Pipeline artifacts are tied 677 | > to the pipeline that they're created in. You can use them within the pipeline 678 | > and download them from the build, as long as the build is retained. Pipeline 679 | > artifacts are the new generation of build artifacts. They take advantage of 680 | > existing services to dramatically reduce the time it takes to store outputs 681 | > in your pipelines. Only available in Azure DevOps Services. 682 | 683 | The "Pipeline artifacts are the new generation of build artifacts" makes me 684 | think maybe I should be producing pipeline artifacts instead of build 685 | artifacts, but build artifacts have been satisfactory so far. Publishing the 686 | APKs as a build artifact makes it easy for me to download the APKs generated by 687 | a build, and that's what I wanted. See 688 | [this screenshot](Screenshots/published artifacts.png) 689 | for how the web interface looks for displaying build artifacts, which can be 690 | downloaded by clicking on them. 691 | 692 | The demo's 693 | [`PublishBuildArtifacts`](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/publish-build-artifacts?view=azure-devops) 694 | step for APKs... 695 | ``` 696 | - task: PublishBuildArtifacts@1 697 | displayName: 'publish APK artifacts' 698 | inputs: 699 | artifactName: 'apks' 700 | ``` 701 | 702 | Previously, an inline powershell script copied an unsigned APK file to 703 | `Build.ArtifactStagingDirectory`, and then `AndroidSigning` task created its 704 | APK and idsig outputs in `Build.ArtifactStagingDirectory`. 705 | `PublishBuildArtifacts`'s `pathToPublish` input defaults to publishing the 706 | directory `Build.ArtifactStagingDirectory`, so the default works out. 707 | `PublishBuildArtifact`'s 708 | [source code](https://github.com/microsoft/azure-pipelines-tasks/blob/f332a4b07713aaa94365fc741127618376a89304/Tasks/PublishBuildArtifactsV1/publishbuildartifacts.ts#L70) 709 | suggests to me that published files are not removed, so keep that in mind when 710 | doing multiple publishes. 711 | 712 | When you download the `apks` artifact, the download will be a zip file named 713 | `apks.zip`, which will contain an `apks` folder that will contain all the 714 | published files. 715 | 716 | Note that `pathToPublish` does not support wildcards. 717 | 718 | The demo does not specify the `publishLocation` input value, so the default of 719 | `container` is being used. I'm not sure what a `container` is, and I can't 720 | find anything that offers an explanation. There is this 721 | [MS Doc about container jobs](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/container-phases?view=azure-devops), 722 | but it talks about containers in the Docker sense. The `publishLocation` input 723 | reference says the `container` option will "store the artifact in Azure 724 | Pipelines" and that sounds good, and does make the artifact available for 725 | [looking at and downloading](https://docs.microsoft.com/en-us/azure/devops/pipelines/artifacts/build-artifacts?view=azure-devops&tabs=yaml#explore-download-and-deploy-your-artifacts) 726 | when I view the build run. The alternate option for `publishLocation` is 727 | `filePath`, which copies the artifacts to "a file share", which I guess you'd 728 | have to set up 729 | 730 | ## Build And Run Unit Tests ## 731 | To build and run unit tests, 732 | [`DotNetCoreCLI`](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/dotnet-core-cli?view=azure-devops) 733 | will take care of... 734 | * Building the test project and its dependencies. 735 | * Discovering and running the tests in the test project. 736 | * Publishing the test results so you can see and explore them in Azure DevOps's 737 | web interface. This includes the build being marked with something like "90% 738 | of tests passing". 739 | 740 | One requirement is that your test project is .NET Core or .NET 5. (Currently, 741 | "dotnet test" does not support Mono, but that may change.) Even if you get 742 | "dotnet test" to work on your Windows machine by making the project SDK-style ( 743 | [format article](https://docs.microsoft.com/en-us/nuget/resources/check-project-format), 744 | [overview article](https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview)), 745 | it won't work on the MacOS agent; you'll get errors about not having the 746 | references assemblies... 747 | ``` 748 | ##[error]/Users/runner/.dotnet/sdk/3.1.404/Microsoft.Common.CurrentVersion.targets(1177,5): Error MSB3644: The reference assemblies for .NETFramework,Version=v5.0 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks 749 | /Users/runner/.dotnet/sdk/3.1.404/Microsoft.Common.CurrentVersion.targets(1177,5): error MSB3644: The reference assemblies for .NETFramework,Version=v5.0 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks [/Users/runner/work/1/s/XamarinPipelineDemo.NUnit/XamarinPipelineDemo.NUnit.csproj] 750 | ``` 751 | 752 | The step for unit tests is... 753 | ``` 754 | - task: DotNetCoreCLI@2 755 | displayName: 'unit tests' 756 | inputs: 757 | command: 'test' 758 | projects: '**/*NUnit*.csproj' 759 | configuration: '$(buildConfiguration)' 760 | ``` 761 | 762 | The `projects` 763 | [input](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/dotnet-core-cli?view=azure-devops#arguments) 764 | supports path wildcards. The code we are testing is already built with the 765 | `Release` build configuration, so if we build our test project with the same 766 | build configuration, we won't have to rebuild our dependencies. 767 | 768 | Just so you know, if you dig in to the 769 | [`dotnet test` doc](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test), 770 | the 771 | [configuration option](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test#options) 772 | defaults to `Debug`. The default build configuration is `Debug` for `msbuild` 773 | and other `dotnet` commands as well. 774 | 775 | Remember that 776 | [VSTest task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/vstest?view=azure-devops) 777 | is not available on MacOS agents. If you need an alternative to 778 | `DotNetCoreCLI` for testing, you'd have to do... 779 | * [`MSBuild` task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/msbuild?view=azure-devops) 780 | to build the needed projects. 781 | * A [script task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/powershell?view=azure-devops): 782 | to run 783 | [`nunit3-console`](https://docs.nunit.org/articles/nunit/running-tests/Console-Command-Line.html) 784 | (already installed on MacOS agents) to run the tests. 785 | * [`PublishTestResults` task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/publish-test-results?view=azure-devops&tabs=trx%2Cyaml) 786 | to publish the test results so you can explore them. 787 | * [`PublishCodeCoverageResults` task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/publish-code-coverage-results?view=azure-devops) 788 | if you want to also see code coverage results. 789 | 790 | ## UI Tests In Azure DevOps ## 791 | ### Set Up And Start Android Emulator ### 792 | For setting up and starting the Android emulator, there are some good examples out there. 793 | 794 | Eric Labelle's 795 | "[Android UI Testing in Azure DevOps](https://medium.com/genetec-tech/android-ui-testing-in-azure-devops-81bbe7cea9fd)" 796 | article is for native Android apps, not Xamarin Android. The article covers 797 | more than just setting up the Android emulator. It talks about caching the 798 | AVD. I found that caching the AVD took the same or longer than just 799 | downloading the AVD fresh, but maybe I was doing something wrong. 800 | 801 | Jan Piotrowski's 802 | [azure-pipelines-android\_emulator repo](https://github.com/janpio/azure-pipelines-android_emulator) 803 | is good in that it gives you a pipeline yaml file with steps definitions for setting up and starting the Android emulator. 804 | 805 | The MS Docs article 806 | "[Build, Test, And Deploy Android Apps](https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/android?view=azure-devops)" 807 | has a 808 | [section on starting the Android emulator](https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/android?view=azure-devops#test-on-the-android-emulator). 809 | 810 | You can see that the bash code in these articles are all pretty much the same. 811 | I think they're all derived from Andrey Mitsyk's 812 | [comment](https://github.com/MicrosoftDocs/azure-devops-docs/issues/1677#issuecomment-433858827) 813 | on the `azure-devops-docs` issue thread about missing Android emulator 814 | documentation. 815 | 816 | I made a few changes to Jan Piotrowski's pipeline steps for this demo... 817 | ``` 818 | variables: 819 | - name: adb 820 | value: '$ANDROID_HOME/platform-tools/adb' 821 | - name: emulator 822 | value: '$ANDROID_HOME/emulator/emulator' 823 | 824 | # .. lots of stuff omitted here ... 825 | 826 | - task: MSBuild@1 827 | displayName: 'build ui tests' 828 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 829 | inputs: 830 | solution: '**/*UITest*.csproj' 831 | configuration: '$(buildConfiguration)' 832 | 833 | - bash: | 834 | set -o xtrace 835 | $ANDROID_HOME/tools/bin/sdkmanager --list 836 | displayName: 'list already installed Android packages' 837 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 838 | 839 | - bash: | 840 | set -o xtrace 841 | echo "y" | $ANDROID_HOME/tools/bin/sdkmanager --install 'system-images;android-30;google_apis;x86' 842 | displayName: 'install Android image' 843 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 844 | 845 | - bash: | 846 | set -o xtrace 847 | $(emulator) -list-avds 848 | echo "no" | $ANDROID_HOME/tools/bin/avdmanager create avd -n uitest_android_emulator -k 'system-images;android-30;google_apis;x86' --force 849 | $(emulator) -list-avds 850 | displayName: 'create AVD' 851 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 852 | 853 | - bash: | 854 | set -o xtrace 855 | $(adb) devices 856 | nohup $(emulator) -avd uitest_android_emulator -no-snapshot -no-boot-anim -gpu auto -qemu > /dev/null 2>&1 & 857 | displayName: 'start Android emulator' 858 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 859 | 860 | - bash: | 861 | set -o xtrace 862 | $(adb) wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82' 863 | $(adb) devices 864 | displayName: 'wait for Android emulator' 865 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 866 | timeoutInMinutes: 5 867 | 868 | ``` 869 | 870 | The `set -o xtrace` lines are so that script lines are 871 | [printed](https://linuxconfig.org/how-to-debug-bash-scripts) 872 | before they are executed. 873 | 874 | I wanted it to be easy to choose emulator/AppCenter/none for UI testing, so you 875 | can see the `condition` of steps depending on `wantEmulatorUITests`. 876 | 877 | Some people like to put their UI test build step right after starting the 878 | Android emulator (and before the wait-for-Android-emulator step) to make better 879 | use of the long time that the Android emulator takes to get ready. 880 | 881 | If there is an Android device that is especially beneficial to do emulator UI 882 | tests on, you can create an 883 | [AVD](https://developer.android.com/studio/run/managing-avds) 884 | for that device. The 885 | [avdmanager command line reference](https://developer.android.com/studio/command-line/avdmanager) 886 | seems incomplete, but googling will get you 887 | [some avdmanager examples](https://gist.github.com/mrk-han/66ac1a724456cadf1c93f4218c6060ae#step-2---use-avdmanager-to-create-emulators) 888 | to learn from. 889 | 890 | I've read that unsigned APKs can be installed on emulators, but I got the 891 | following error when trying to do a `adb install unsigned.apk`... 892 | ``` 893 | adb: failed to install /Users/runner/work/1/a/unsigned.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed collecting certificates for /data/app/vmdl1550487669.tmp/base.apk: Failed to collect certificates from /data/app/vmdl1550487669.tmp/base.apk: Attempt to get length of null array] 894 | ``` 895 | So, install a signed APK on your emulator, even if the APK is signed by the 896 | debug keystore. 897 | 898 | 899 | ### Build UI Tests ### 900 | Here's the step definition again... 901 | ``` 902 | - task: MSBuild@1 903 | displayName: 'build ui tests' 904 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 905 | inputs: 906 | solution: '**/*UITest*.csproj' 907 | configuration: '$(buildConfiguration)' 908 | ``` 909 | 910 | The situation is pretty simple once you learn that you can't use 911 | `DotNetCoreCLI` task for Xamarin.UITest project. 912 | [MSBuild task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/msbuild?view=azure-devops) 913 | is the only build task available on the MacOS agent (other than doing some 914 | script task that calls `msbuild`). You probably want to match the same build 915 | configuration that was used to compile the other projects, unless your UI test 916 | project has no dependencies that were built before. 917 | 918 | ### Run Emulator UI Tests ### 919 | 920 | [Currently](https://github.com/mono/mono/issues/6984), 921 | `dotnet test` does not work with Mono, so the `DotNetCoreCLI` task does not 922 | work on the MacOS agent for running Xamarint.UITest tests. So, you have to run 923 | the tests via 924 | [`nunit3-console`](https://docs.nunit.org/articles/nunit/running-tests/Console-Command-Line.html) 925 | and publish the test results via the 926 | [`PublishTestResults` task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/publish-test-results?view=azure-devops&tabs=trx%2Cyaml) 927 | (which is nicely integrated into the Azure DevOps web interface for that build 928 | and for analysis across builds). For troubleshooting, you may want to publish 929 | the detailed UI test log (not the same thing as test results). 930 | 931 | Here are the steps and relevant variable definitions... 932 | ``` 933 | variables: 934 | - name: uiTestDir 935 | value: '$(Build.SourcesDirectory)/XamarinPipelineDemo.UITest' 936 | - name: uiTestResultPath 937 | value: '$(Build.ArtifactStagingDirectory)/uitest_result.xml' 938 | - name: uiTestLogPath 939 | value: '$(Build.ArtifactStagingDirectory)/uitest.log' 940 | 941 | # ... lots of stuff omitted here ... 942 | 943 | - pwsh: | 944 | Set-PSDebug -Trace 1 945 | $env:UITEST_APK_PATH = "$(finalApkPathSigned)" 946 | $testAssemblies = Get-Item "$(uiTestDir)/bin/$(buildConfiguration)/XamarinPipelineDemo.UITest*.dll" 947 | nunit3-console $testAssemblies --output="$(uiTestLogPath)" --result="$(uiTestResultPath)" 948 | displayName: 'run ui tests' 949 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 950 | continueOnError: true 951 | timeoutInMinutes: 120 952 | ``` 953 | 954 | Note that `nunit3-console` 955 | [defaults](https://docs.nunit.org/articles/nunit/running-tests/Console-Command-Line.html#description) 956 | to putting the test results into `./TestResult.xml` file, but the demo specifies a path for `--result`. 957 | 958 | The demo app is installed as package `com.demo.XamarinPipelineDemo`, and the UI 959 | tests need to install package `com.demo.XamarinPipelineDemo.test` and the 960 | signatures of those two packages must match. Look at 961 | [`AppInitializer.cs`](XamarinPipelineDemo.UITest/AppInitializer.cs) 962 | for how it's done, but the basics is you either use 963 | [`ApkFile`](https://docs.microsoft.com/en-us/dotnet/api/xamarin.uitest.configuration.androidappconfigurator.apkfile?view=xamarin-uitest-sdk) 964 | by itself or 965 | [`InstalledApp`](https://docs.microsoft.com/en-us/dotnet/api/xamarin.uitest.configuration.androidappconfigurator.installedapp?view=xamarin-uitest-sdk) 966 | and 967 | [`KeyStore`](https://docs.microsoft.com/en-us/dotnet/api/xamarin.uitest.configuration.androidappconfigurator.keystore?view=xamarin-uitest-sdk) 968 | together. The `ApkFile` method is simpler. It even takes care of installing 969 | the APK onto the emulator. 970 | 971 | If the app package and the test package don't have the same signature, you'll get an error like this: 972 | ``` 973 | System.Exception : Failed to execute: /Users/runner/Library/Android/sdk/platform-tools/adb -s emulator-5554 shell am instrument com.demo.XamarinPipelineFiddle.test/sh.calaba.instrumentationbackend.ClearAppData2 - exit code: 1 974 | java.lang.SecurityException: Permission Denial: starting instrumentation ComponentInfo{com.demo.XamarinPipelineFiddle.test/sh.calaba.instrumentationbackend.ClearAppData2} from pid=5635, uid=5635 not allowed because package com.demo.XamarinPipelineFiddle.test does not have a signature matching the target com.demo.XamarinPipelineFiddle 975 | ``` 976 | 977 | If you get the error `System.Exception : Timed out waiting for result of ClearAppData2` in your job log, and the detailed UI test log file contains... 978 | ``` 979 | AdbArguments: '-s emulator-5554 shell run-as com.demo.XamarinPipelineDemo.test ls "/data/data/com.demo.XamarinPipelineDemo.test/files/calabash_failure.out"'. 980 | Finished with exit code 1 in 184 ms. 981 | ls: /data/data/com.demo.XamarinPipelineDemo.test/files/calabash_failure.out: No such file or directory 982 | ``` 983 | ...then the most likely explanation is that your app crashed. When you 984 | encounter this error, locally try out UI tests with the exact APK file that the 985 | pipeline was using on an Android emulator (not a real Android device). 986 | The crash might happen only in `Release` build configuration, or something 987 | else. Be aware that running the UI test on an Android emulator requires x86 to 988 | be one of the supported architectures (Android project properties => Android 989 | Options => Advanced => Supported Architectures). 990 | 991 | The job log having the error `System.Exception : Post to endpoint '/ping' 992 | failed after 100 retries. No http result received` is most likely due to an app 993 | crash or not including x86 as a supported architecture. This error can also be 994 | due to problems with the agent pool (as in it's not your fault, you might have 995 | to re-run the build a few times and hopefully the problem passes). 996 | 997 | Also, these "ClearAppData2" and "post to endpoint" errors might be followed by 998 | a `TearDown: System.NullReferenceException` error, and that is because the test 999 | runner still calls the test `TearDown` method, which might try to use the IApp 1000 | object that was supposed to be set to the result of `AppInitializer.StartApp`. 1001 | The real problem is the earlier errors, not the exception in `TearDown`. 1002 | 1003 | The error `Tcp transport error` is something I've only seen due to agent pool 1004 | problems. I just had to wait and retry a few times. 1005 | 1006 | ### Publish Emulator UI Tests ### 1007 | 1008 | Running `nunit3-console` will run the tests and generate a test result xml file 1009 | and a test log file, but we still need to publish at least the test results... 1010 | ``` 1011 | - task: PublishBuildArtifacts@1 1012 | displayName: 'publish ui test log artifact' 1013 | inputs: 1014 | artifactName: 'UI test log' 1015 | pathToPublish: '$(uiTestLogPath)' 1016 | continueOnError: true 1017 | 1018 | - task: PublishTestResults@2 1019 | condition: eq(variables.wantEmulatorUiTests, true) 1020 | inputs: 1021 | testRunTitle: 'Android UI Test Run' 1022 | testResultsFormat: 'NUnit' 1023 | testResultsFiles: '$(uiTestResultPath)' 1024 | # Android tests may randomly fail because of the System UI not responding (if you're using Prism); 1025 | # see https://github.com/PrismLibrary/Prism/issues/2099 ; 1026 | # tests may also fail due to pool agent problems; 1027 | # using the following line still makes builds have warning status when UI tests fail 1028 | # failTaskOnFailedTests: false 1029 | ``` 1030 | 1031 | Publishing the UI test log as a general build artifact is for troubleshooting; 1032 | the normal job log is pretty helpful, but you need to look at the UI test log 1033 | in order to see any console printing your UI tests did. 1034 | 1035 | Publishing the test results makes them nicely integrated with the Azure DevOps 1036 | web interface and associated with the build. 1037 | 1038 | Sometimes UI tests fail due to things like agent problems. Up to you whether 1039 | you want to treat failing UI tests as warning or failure via the 1040 | `failTaskOnFailedTests` input. 1041 | 1042 | ## UI Tests In App Center ## 1043 | ### Set Up App Center ### 1044 | You'll need to have an App Center account, and you'll want to "Add new app". In 1045 | the app's "Build" section, you'll select Azure DevOps for the service and 1046 | select your Azure DevOps repo. 1047 | 1048 | Then, the "Build" section will show you the repo branches, and you want to 1049 | click on the wrench icon for the appropriate branch (most likely `main` or 1050 | `master`). The wrench icon will be invisible until you hover over the branch 1051 | info box. 1052 | 1053 | Clicking on the wrench icon will bring you to build configuration settings for 1054 | that branch; choose settings that make sense to you, but you will have to 1055 | enable "Sign builds" and supply the appropriate keystore file and info. 1056 | 1057 | Then go to the "Test" section, "Device sets" subsection, and create a new 1058 | device set. You'll be using the device set name later. 1059 | 1060 | ### Experiment With `appcenter` CLI ### 1061 | You should probably install the 1062 | [`appcenter` CLI](https://github.com/microsoft/appcenter-cli) 1063 | and get some successful test runs with that before you try to use the 1064 | `AppCenterTest` task in a pipeline. The `appcenter` CLI allows for much faster 1065 | iteration, especially at the beginning. The CLI instantly tells you that you 1066 | forgot a required argument, and the Azure DevOps pipeline might take minutes to 1067 | make the same complaint. The CLI also uses your local files (APK, dlls, 1068 | test-cloud.exe), so you don't have to wait for a pipeline build process either. 1069 | 1070 | Check out 1071 | [`LocalScripts/appcenter_uitest_run.ps1`](LocalScripts/appcenter_uitest_run.ps1) 1072 | for a working invokation of `appcenter` CLI. Note that `appcenter` CLI needs 1073 | `ANDROID_HOME` and `JAVA_HOME` environment variables defined, which is taken 1074 | care of by 1075 | [`LocalScripts/common.ps1`](LocalScripts/common.ps1). 1076 | 1077 | For convenience, here's a PowerShell snippet that calls `appcenter` CLI (the 1078 | only optional argument is `--test-output-dir`): 1079 | ``` 1080 | appcenter test run uitest ` 1081 | --app "$orgName/$appName" ` 1082 | --app-path "$env:UITEST_APK_PATH" ` 1083 | --devices "$orgName/demo_device_set" ` 1084 | --test-series "master" ` 1085 | --locale "en_US" ` 1086 | --build-dir "..\$uiTestProjName\bin\$BuildConfiguration" ` 1087 | --uitest-tools-dir "..\$uiTestProjName\bin\$BuildConfiguration" ` 1088 | --test-output-dir $testOutputDir 1089 | ``` 1090 | 1091 | Remember, you need to build the Xamarin.UITest project before you call 1092 | `appcenter`; that's why the `appcenter_uitest_run.ps1` script has a build step. 1093 | 1094 | ### Run App Center UI Tests ### 1095 | The interplay between Azure DevOps and App Center is confusing, and I still 1096 | don't fully understand it, but I will go over how I got my Azure DevOps pipeline 1097 | to execute UI tests on real devices in App Center using the 1098 | [`AppCenterTest` task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/app-center-test?view=azure-devops). 1099 | 1100 | Here are the relevant pipeline steps for running the App Center UI tests 1101 | (publishing test resuls in next subsection)... 1102 | ``` 1103 | ################################################################################ 1104 | # UI tests, preparatory steps common to AppCenter and emulator 1105 | 1106 | - task: MSBuild@1 1107 | displayName: 'build ui tests' 1108 | condition: > 1109 | and( 1110 | succeeded(), 1111 | or( 1112 | eq(variables.wantAppCenterUiTests, true), 1113 | eq(variables.wantEmulatorUiTests, true) 1114 | ) 1115 | ) 1116 | inputs: 1117 | solution: '**/*UITest*.csproj' 1118 | configuration: '$(buildConfiguration)' 1119 | 1120 | ################################################################################ 1121 | # AppCenter UI tests 1122 | 1123 | # default nodejs version (v12) is not compatible with stuff used in AppCenterTest task 1124 | # https://github.com/microsoft/appcenter-cli/issues/696#issuecomment-553218361 1125 | - task: UseNode@1 1126 | displayName: 'Use Node 10.15.1' 1127 | condition: and(succeeded(), eq(variables.wantAppCenterUiTests, true)) 1128 | inputs: 1129 | version: 10.15.1 1130 | 1131 | - task: AppCenterTest@1 1132 | condition: and(succeeded(), eq(variables.wantAppCenterUiTests, true)) 1133 | continueOnError: true 1134 | inputs: 1135 | appFile: '$(finalApkPathSigned)' 1136 | appSlug: 'JacobEgnerDemos/XamarinPipelineDemo' # orgname or username, then '/', then app name 1137 | devices: 'JacobEgnerDemos/demo_device_set' # uses same orgname or username, then '/', then device set name 1138 | frameworkOption: 'uitest' 1139 | runOptions: --test-output-dir "$(appCenterOutputDir)" # only needed if publishing test results in Azure DevOps 1140 | serverEndpoint: 'AppCenterConnectionUserBasedFullAccess' # make a App Center user API token, then add service connection in Azure DevOps 1141 | uiTestBuildDirectory: '$(uiTestAssemblyDir)' # directory that contains the uitest assemblies, not the build directory 1142 | uiTestToolsDirectory: '$(uiTestAssemblyDir)' # build process puts test-cloud.exe in assembly dir 1143 | ``` 1144 | 1145 | If you get errors like "Error: Command test prepare uitest ... is invalid", 1146 | then you have a nodejs version problem. Unfortunately, (as of time of 1147 | writing), Azure DevOps pipelines default to nodejs version 12, but 1148 | AppCenterTest task requires nodejs version 10. Thus, the demo uses the 1149 | `UseNode` task to set nodejs to version 10.15.1, just like this 1150 | [appcenter-cli issue 696 thread](https://github.com/microsoft/appcenter-cli/issues/696#issuecomment-553218361) 1151 | suggests. 1152 | 1153 | Strangely enough, I can't find `UseNode` task doc via searching or looking 1154 | through all the other 1155 | [task docs](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/?view=azure-devops). 1156 | There's 1157 | [`NodeTool`](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/tool/node-js?view=azure-devops) 1158 | task doc, but looking at the source code of these tasks 1159 | ([`usenode.ts`](https://github.com/microsoft/azure-pipelines-tasks/blob/master/Tasks/UseNodeV1/usenode.ts), 1160 | [`nodetool.ts`](https://github.com/microsoft/azure-pipelines-tasks/blob/master/Tasks/NodeToolV0/nodetool.ts)) 1161 | and comments, they don't seem to do the same thing. 1162 | 1163 | The 1164 | [`AppCenterTest` task reference](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/app-center-test?view=azure-devops) 1165 | suggests that the `prepareTests` input defaults to true and therefore default 1166 | behavior is to build the UI test project, but I never got that to work and had 1167 | to use a separate `MSBuild` task. If the UI test project is not built, I think 1168 | the first error you'll get is about not finding `test-cloud.exe`. 1169 | 1170 | If you are successfully building the UI test project and still get a 1171 | `test-cloud.exe` related error like this... 1172 | ``` 1173 | Preparing tests... failed. 1174 | Error: Cannot find test-cloud.exe, which is required to prepare UI tests. 1175 | We have searched for directory "packages\Xamarin.UITest.*\tools" inside "D:\" and all of its parent directories. 1176 | Please use option "--uitest-tools-dir" to manually specify location of this tool. 1177 | Minimum required version is "2.2.0". 1178 | ##[error]Error: D:\a\_tasks\AppCenterTest_ad5cd22a-be4e-48bb-adce-181a32432da5\1.152.3\node_modules\.bin\appcenter.cmd failed with return code: 3 1179 | ``` 1180 | ...then you need to be sure that your `AppCenterTest` task's 1181 | `uiTestToolsDirectory` input is set to a folder that contains `test-cloud.exe`. 1182 | There are a few existing discussions (like 1183 | [this azure-pipelines-tasks issue discussion](https://github.com/microsoft/azure-pipelines-tasks/issues/6868)) 1184 | where people suggest pointing to Xamarin.UITest's nuget package folder, 1185 | but you don't need to do that. When you build your Xamarin.UITest project, 1186 | `test-cloud.exe` is put into the output folder alongside the generated dlls. 1187 | So, I set `uiTestToolsDirectory` to that output folder. 1188 | 1189 | Likewise, you might get an error complaining about a missing `nunit.framework.dll`... 1190 | ``` 1191 | Unable to find the nunit.framework.dll in the assembly directory. In Xamarin Studio you may have to right-click on the nunit.framework reference and choose Local Copy for it to be included in the output directory. 1192 | 1193 | Preparing tests... failed. 1194 | Error: Cannot prepare UI Test artifacts using command: mono /Users/runner/work/1/s/packages/xamarin.uitest/3.0.12/tools/test-cloud.exe prepare "/Users/runner/work/1/a/XamarinPipelineDemo_20210108.2_1_Signed.apk" --assembly-dir "/Users/runner/work/1/s/XamarinPipelineDemo.UITest" --artifacts-dir "/Users/runner/work/1/a/AppCenterTest". 1195 | 1196 | The NUnit library was not found, please try again. If you can't work out how to fix this issue, please contact support. 1197 | ``` 1198 | ...because `AppCenterTest` task is looking for 1199 | `nunit.framework.dll` in the assembly directory, and isn't finding it. It's 1200 | possible you just haven't built the UITest project yet, or didn't assign the 1201 | `uiTestBuildDirectory` input correctly. Even though the input name suggests a 1202 | build directory, the reference page describes the input as "Path to directory 1203 | with built test assemblies". So, I set both `uiTestToolsDirectory` and 1204 | `uiTestBuildDirectory` to my UI test project's output directory where the dlls 1205 | are generated. 1206 | 1207 | You have to supply an `appSlug` input, or you'll get the error message 1208 | `Error: Input required: appSlug`. The reference doc for that input says you 1209 | need to specify it with format `{username}/{app_identifier}` and you can learn the 1210 | values by looking at the URL of your app page in App Center: 1211 | `https://appcenter.ms/users/{username}/apps/{app_identifier}` 1212 | but `username` can also be your organization name and the URL might have format 1213 | `https://appcenter.ms/orgs/{orgname}/apps/{app_identifier}`. 1214 | My App Center app URL is 1215 | `https://appcenter.ms/orgs/JacobEgnerDemos/apps/XamarinPipelineDemo`, 1216 | so I used `appSlug: 'JacobEgnerDemos/XamarinPipelineDemo'`. I've personally 1217 | tried this, and later I found 1218 | [this MS Docs page](https://docs.microsoft.com/en-us/appcenter/api-docs/#find-owner_name-and-app_name-from-an-app-center-url) 1219 | that agrees. 1220 | 1221 | For the `devices` input, the `AppCenterTest` task reference is not helpful. The 1222 | [Starting A Test Run article](https://docs.microsoft.com/en-us/appcenter/test-cloud/starting-a-test-run) 1223 | says that for the `appcenter` cli, the `devices` argument can be the 1224 | hexadecimal value or "the ID ... generated from the device set name". I had to 1225 | experiment to figure out that the ID is not just the device set name. The 1226 | device set ID is like the app slug: username or orgname, then '/', then device 1227 | set name. My device set name is `demove_device_set` but the URL 1228 | replaces the underscores with hyphens: 1229 | `https://appcenter.ms/orgs/JacobEgnerDemos/apps/XamarinPipelineDemo/test/device-sets/demo-device-set`, 1230 | and I once ran an App Center test run where I used hypens in the device set id, 1231 | and it worked. 1232 | 1233 | I recommend the named device set. In App Center, under `Test` and `Devices sets`, 1234 | create a device set and name it. 1235 | 1236 | If you don't want to use a named device set, you can determine the proper 1237 | hexadecimal number by creating a test run in App Center, choosing a set of 1238 | devices, and on the "submit" step, you'll be shown a command to "upload and 1239 | schedule tests", and that command will contain something like `--devices 1240 | c2e61997`. You don't actually have to submit the test run; you can just abandon 1241 | the creation of the test run once you see the hexadecimal number. 1242 | 1243 | The `serverEndpoint` input is required when using the default 1244 | `credentialsOption` value of `serviceEndpoint`. The `serverEndpoint` input 1245 | needs to specify the "service connection for AppCenter". Steps to create and 1246 | use a service connection... 1247 | * In App Center, 1248 | [create a *full-access* App Center *user* API token](https://docs.microsoft.com/en-us/appcenter/api-docs/#creating-an-app-center-user-api-token). 1249 | * In Azure DevOps, go to your project settings, then pipelines section, then service connections entry. 1250 | * Create a new service connection. 1251 | * For service connection type, scroll down to the bottom and select "Visual 1252 | Studio App Center". 1253 | * Supply the API token and name the service connection (ex: 1254 | `AppCenterConnectionUserBasedFullAccess`) 1255 | * You'll either preemptively grant permission to all pipelines to use this 1256 | service connection, or you'll have to click some stuff to approve the first 1257 | time that a pipeline uses the service connection. 1258 | * In pipeline definition, use the name of the service connection for your 1259 | `serverEndpoint` input. 1260 | 1261 | A read-only token will give you an `Error: forbidden` error. You 1262 | need a full-access token. 1263 | 1264 | It would be nice to make an app token (which only has access to 1265 | one App Center app), but as of 2021-Jan, app tokens do not work. You'll get a 1266 | `Error: empty email address` error if using an app token. You need to use a user 1267 | token, which is associated with a user and has access to everything that user 1268 | has access to. 1269 | [This stackoverflow discussion](https://stackoverflow.com/questions/64008037/error-empty-email-address-doing-postbuild-in-appcenter) 1270 | says that Microsoft's official advice as of 2020-Oct is: 1271 | 1272 | > For test you need to use the user level token only, app level token was not 1273 | supported. Our test team was already working on this but currently there is no 1274 | ETA on it. 1275 | 1276 | Once they fix the app token issue, you can follow 1277 | [these instructions](https://docs.microsoft.com/en-us/appcenter/api-docs/#creating-an-app-center-app-api-token). 1278 | to make an App Center app API token. 1279 | 1280 | The `artifactsDirectory` input for `AppCenterTest` task defaults to 1281 | `$(Build.ArtifactStagingDirectory)/AppCenterTest`. With normal inputs, the 1282 | following files are output to that folder: 1283 | * apps (folder) 1284 | * XamarainPipelineDemo_20201231.1_9.apk (apk file put on devices to test) 1285 | * AndroidTestServer.apk 1286 | * manifest.json (mentions found dlls, found test methods, excluded tests, does 1287 | not contain test results) 1288 | * the rest of the files are all the dlls from the `uiTestBuildDirectory` 1289 | * nunit*.dll (a bunch of NUnit dlls for running the tests) 1290 | * Xamarin.UITest.dll 1291 | * XamarinPipelineDemo.UITest.dll (the dll made from the ui test project) 1292 | 1293 | These outputs are not useful. You already have these files elsewhere, except 1294 | for `manifest.json`, which is still not useful. 1295 | 1296 | #### Publish App Center UI Tests In Azure DevOps #### 1297 | By default, the results of your `AppCenterTask` tests are viewable in App 1298 | Center. If you look at the log of the `AppCenterTest` task in Azure DevOps, 1299 | you'll see a very brief "X passed, Y failed" summary and a link to a full test 1300 | report at App Center. 1301 | 1302 | In order to publish the full test results in Azure DevOps, you need to do a few 1303 | extra steps. Here are the relevant pipeline steps (`AppCenterTest` is repeated 1304 | for your convenience)... 1305 | ``` 1306 | - task: AppCenterTest@1 1307 | condition: and(succeeded(), eq(variables.wantAppCenterUiTests, true)) 1308 | continueOnError: true 1309 | inputs: 1310 | appFile: '$(finalApkPathSigned)' 1311 | appSlug: 'JacobEgnerDemos/XamarinPipelineDemo' # orgname or username, then '/', then app name 1312 | devices: 'JacobEgnerDemos/demo_device_set' # uses same orgname or username, then '/', then device set name 1313 | frameworkOption: 'uitest' 1314 | runOptions: --test-output-dir "$(appCenterOutputDir)" # only needed if publishing test results in Azure DevOps 1315 | serverEndpoint: 'AppCenterConnectionUserBasedFullAccess' # make a App Center user API token, then add service connection in Azure DevOps 1316 | uiTestBuildDirectory: '$(uiTestAssemblyDir)' # directory that contains the uitest assemblies, not the build directory 1317 | uiTestToolsDirectory: '$(uiTestAssemblyDir)' # build process puts test-cloud.exe in assembly dir 1318 | 1319 | - pwsh: Expand-Archive "$(appCenterOutputDir)/nunit_xml_zip.zip" -DestinationPath "$(appCenterTestResultsDir)" 1320 | displayName: 'unzip App Center test results zip' 1321 | condition: and(succeededOrFailed(), eq(variables.wantAppCenterUiTests, true)) 1322 | continueOnError: true 1323 | 1324 | - task: PublishTestResults@2 1325 | displayName: 'simple-publish App Center UI test results' 1326 | condition: and(succeededOrFailed(), eq(variables.wantAppCenterUiTests, true)) 1327 | inputs: 1328 | testRunTitle: 'Android App Center UI Test Run (simple publish)' 1329 | testResultsFormat: 'NUnit' 1330 | testResultsFiles: '$(appCenterTestResultsDir)/*.xml' 1331 | 1332 | - pwsh: | 1333 | Get-ChildItem "$(appCenterTestResultsDir)/*.xml" | ForEach-Object { Write-Output ` 1334 | ( "##vso[results.publish " ` 1335 | + "runTitle=Android App Center UI Test Run $($_.BaseName);" ` 1336 | + "resultFiles=$($_.FullName);" ` 1337 | + "type=NUnit;" ` 1338 | + "mergeResults=false;" ` 1339 | + "publishRunAttachments=true;" ` 1340 | + "failTaskOnFailedTests=false;" ` 1341 | + "testRunSystem=VSTS - PTR;" ` 1342 | + "]" ` 1343 | )} 1344 | displayName: 'complicated-publish App Center UI test results with device name' 1345 | condition: and(succeededOrFailed(), eq(variables.wantAppCenterUiTests, true)) 1346 | ``` 1347 | 1348 | First, we had to use `AppCenterTest`'s `runOptions` input to specify a 1349 | directory to output the test results; without the `--test-output-dir` option, 1350 | test results won't be output to file at all. 1351 | 1352 | To find out about `--test-output-dir`, I had to dig through the 1353 | [`appcenter` CLI source code](https://github.com/microsoft/appcenter-cli). 1354 | The README.md hints we are interested in the `appcenter test run uitest` 1355 | command, but no documentation on options. I had to dig down into 1356 | [`uitest.cs`](https://github.com/microsoft/appcenter-cli/blob/master/src/commands/test/run/uitest.ts) 1357 | to find 1358 | [use of `this.testOutputDir`](https://github.com/microsoft/appcenter-cli/blob/28aa11bccfc89907d53b37b35b7015f78d4a39b4/src/commands/test/run/uitest.ts#L103). 1359 | Chasing `testOutputDir` around the code base made me think it was worth trying out, and it worked. 1360 | 1361 | (Unfortunately, I never got the `--merge-nunit-xml` option to work. The 1362 | option always caused me to get an error and I have filed 1363 | [an issue](https://github.com/microsoft/appcenter-cli/issues/1208). 1364 | Contact me if you ever get it to work.) 1365 | 1366 | Once you specify a `--test-output-dir` to App Center test run, it'll make a 1367 | `nunit_xml_zip.zip` file (other UI test frameworks will have different output 1368 | file names, 1369 | [like `junit_xml_zip.zip` for junit](https://github.com/microsoft/appcenter-cli/blob/28aa11bccfc89907d53b37b35b7015f78d4a39b4/src/commands/test/lib/xml-util-builder.ts)). 1370 | 1371 | That `nunit_xml_zip.zip` will contain xml files for each tested device, named 1372 | like `google_pixel_3_11_nunit_report.xml`. The `11` is from the Android OS 1373 | version, because you might test the Pixel 3 model with Android 10 and Android 11. 1374 | 1375 | Azure DevOps supports a 1376 | [`ExtractFiles` task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/extract-files?view=azure-devops), 1377 | but I went with an inline PowerShell script because I can test out the exact 1378 | behavior on my system, rather than doing multiple pipeline runs to troubleshoot 1379 | whatever I did wrong with the `ExtractFiles` task. 1380 | 1381 | In Azure DevOps, when exploring test results published by the `simple-publish` 1382 | step, the App Center UI test results will be named whatever you supplied as the 1383 | `testRunTitle` input to the `PublishTestResults` task, and contain a "_1" style 1384 | suffix if you tested more than one device. Currently, I don't know of a simple 1385 | way to have the test results specify the used device. 1386 | 1387 | But here is a complicated way to get your App Center test results labeled with 1388 | the device info: do a PowerShell one-liner to publish each test result xml file 1389 | with a test run title that uses the xml file name. The critical ingredient of 1390 | the `PublishTestResults` task is that it will output something like the 1391 | following to the log/stdout: 1392 | `##[debug]Processed: ##vso[results.publish type=NUnit;mergeResults=false;runTitle=Android App Center UI Test Run;publishRunAttachments=true;resultFiles=/Users/runner/work/1/a/AppCenterTest/TestResults/google_pixel_10_nunit_report.xml,/Users/runner/work/1/a/AppCenterTest/TestResults/google_pixel_3_11_nunit_report.xml;failTaskOnFailedTests=false;testRunSystem=VSTS - PTR;]` 1393 | 1394 | This `##vso[results.publish ...` magic spell is similar to how you can set pipeline variables by outputting 1395 | `##vso[task.setvariable variable=someVariable]someValue`, 1396 | 1397 | So, my `complicated-publish` step just outputs that magic spell to stdout for 1398 | each xml file. I don't know of anyone else who has done this. 1399 | 1400 | 1401 | ## Problems Accessing Stuff ## 1402 | You might have someone who can't access something, like build artifacts, 1403 | regardless of permissions (and rememeber there are permissions under project 1404 | settings, then permissions for pipelines, then permissions for EACH pipeline). 1405 | The problem might be their “access level”. If their access level is 1406 | “Stakeholder”, then it probably needs to be changed to "Basic" or better. 1407 | “Basic”. You can check anyone’s organization-specific access level at URLs 1408 | like this: `dev.azure.com/TheAppropriateOrganization/_settings/users` 1409 | 1410 | # Thanks To Those Who Helped Me # 1411 | * James Montemagno, thanks for the huge amount of educational Xamarin content 1412 | you've made, and specifically for the [Mobile App Tasks Azure DevOps 1413 | plugin](https://marketplace.visualstudio.com/items?itemName=vs-publisher-473885.motz-mobile-buildtasks). 1414 | * Online presence: 1415 | [web site](https://montemagno.com/), 1416 | [GitHub](https://github.com/jamesmontemagno), 1417 | [Twitter](https://twitter.com/JamesMontemagno), 1418 | [YouTube](https://www.youtube.com/channel/UCENTmbKaTphpWV2R2evVz2A), 1419 | [Twitch](https://twitch.tv/jamesmontemagno), 1420 | [Xamarin Show](https://channel9.msdn.com/Shows/XamarinShow), 1421 | [DevBlog](https://devblogs.microsoft.com/xamarin/author/jamesmontemagno/), 1422 | * Andrew Hoefling, thanks for the 1423 | "[Azure Pipelines Custom Build Numbers in YAML Templates](https://www.andrewhoefling.com/Blog/Post/azure-pipelines-custom-build-numbers-in-yaml-templates)" 1424 | article and your contribution to the Mobile App Tasks Azure DevOps plugin, 1425 | your 1426 | "[Azure Pipelines Custom Build Numbers in YAML Templates](https://www.andrewhoefling.com/Blog/Post/azure-pipelines-custom-build-numbers-in-yaml-templates)" 1427 | blog post, and leading the 1428 | [Rochester Xamarin Meetup group](https://www.meetup.com/Rochester-Xamarin-Meetup/). 1429 | I look forward to reading more of your blog. 1430 | * online presence: 1431 | [web site](https://www.andrewhoefling.com/), 1432 | [GitHub](https://github.com/ahoefling), 1433 | [Twitter](https://twitter.com/andrew_hoefling), 1434 | * Dan Siegel, thanks for your [Prism](https://prismlibrary.com/) work, your 1435 | educational content, personally helping me with Prism, and pointing me to a 1436 | [UITest-in-pipeline example](https://github.com/PrismLibrary/Prism/tree/master/build) 1437 | to work from. 1438 | * online presence: 1439 | [web site](https://dansiegel.net/), 1440 | [LinkedIn](https://twitch.tv/dansiegel), 1441 | [GitHub](https://github.com/dansiegel), 1442 | [StackOverflow](https://stackoverflow.com/users/5699454/dan-s), 1443 | [Twitter](https://twitter.com/DanJSiegel), 1444 | [YouTube](https://www.youtube.com/dansiegel), 1445 | [Twitch](https://twitch.tv/dansiegel), 1446 | * Jerome Laban, thanks for making the UITest-in-pipeline example, and 1447 | making/[explaining](https://twitter.com/jlaban/status/1338923127615741956) 1448 | the additional example at UnoPlatform. 1449 | * online presence: 1450 | [web site](https://jaylee.org/), 1451 | [GitHub](https://github.com/jeromelaban), 1452 | [Twitter](https://www.twitter.com/jlaban), 1453 | [Twitch](https://twitch.tv/jeromelaban), 1454 | * Jan Piotrowski, thanks for your example pipeline steps for setting up and 1455 | starting the Android emulator. 1456 | * online presence: 1457 | [web site](https://janpiotrowski.de/), 1458 | [GitHub](https://github.com/janpio), 1459 | [Twitter](https://twitter.com/Sujan/), 1460 | * Andrey Mitsyk, thank you for providing the bash script to setup and start the 1461 | Android emulator. 1462 | * online presence: 1463 | [GitHub](https://github.com/AndreyMitsyk) 1464 | * Brian Lagunas, thanks for giving me SO MUCH Xamarin/Prism help and your work 1465 | on Prism. 1466 | * online presence: 1467 | [web site](https://brianlagunas.com/), 1468 | [GitHub](https://github.com/brianlagunas), 1469 | [Twitter](http://twitter.com/brianlagunas), 1470 | [YouTube](https://www.youtube.com/channel/UCC78lt3WRH0bROwiIvw847g), 1471 | [Twitch](https://www.twitch.tv/brianlagunas). 1472 | -------------------------------------------------------------------------------- /Screenshots/pipelines - library - android_demo_var_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/Screenshots/pipelines - library - android_demo_var_group.png -------------------------------------------------------------------------------- /Screenshots/published artifacts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/Screenshots/published artifacts.png -------------------------------------------------------------------------------- /Screenshots/run job summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/Screenshots/run job summary.png -------------------------------------------------------------------------------- /Screenshots/run summary - permission grant popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/Screenshots/run summary - permission grant popup.png -------------------------------------------------------------------------------- /Screenshots/run summary - permissions needed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/Screenshots/run summary - permissions needed.png -------------------------------------------------------------------------------- /Screenshots/run_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/Screenshots/run_summary.png -------------------------------------------------------------------------------- /Screenshots/test explorer - filters cleared.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/Screenshots/test explorer - filters cleared.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Assets/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories) and given a Build Action of "AndroidAsset". 3 | 4 | These files will be deployed with your package and will be accessible using Android's 5 | AssetManager, like this: 6 | 7 | public class ReadAsset : Activity 8 | { 9 | protected override void OnCreate (Bundle bundle) 10 | { 11 | base.OnCreate (bundle); 12 | 13 | InputStream input = Assets.Open ("my_asset.txt"); 14 | } 15 | } 16 | 17 | Additionally, some Android functions will automatically load asset files: 18 | 19 | Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); 20 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/AzureDevOps/AndroidSetVersion.ps1: -------------------------------------------------------------------------------- 1 | # adapted from https://github.com/jamesmontemagno/vsts-mobile-tasks/blob/18c29f6a58bb4194ecddc9bc68e743b041127dd0/tasks/AndroidBumpVersion/task.ps1 2 | # following PoshCode style guide: https://github.com/PoshCode/PowerShellPracticeAndStyle 3 | 4 | param ( 5 | [Parameter(Mandatory)] 6 | [string] 7 | $ManifestPath, 8 | 9 | # manual, long-term adjustment to build id or whatever 10 | [Parameter(Mandatory)] 11 | [int] 12 | $VersionCodeBase, 13 | 14 | # the value that increases each time 15 | [Parameter(Mandatory)] 16 | [int] 17 | $VersionCodeOffset, 18 | 19 | [Parameter(Mandatory)] 20 | [string] 21 | $VersionName, 22 | 23 | [bool] 24 | $PrintFile = $false 25 | ) 26 | 27 | [int] $versionCode = $VersionCodeBase + $VersionCodeOffset 28 | 29 | Write-Output "ENVIRONMENT VARIABLES ----------" 30 | Write-Output "Build_BuildId: $env:Build_BuildId" 31 | Write-Output "Build_BuildNumber: $env:Build_BuildNumber" 32 | Write-Output "SCRIPT VARIABLES ---------------" 33 | Write-Output "ManifestPath: $ManifestPath" 34 | Write-Output "VersionCodeBase: $VersionCodeBase" 35 | Write-Output "VersionCodeOffset: $VersionCodeOffset" 36 | Write-Output "calced versionCode: $versionCode" 37 | Write-Output "VersionName: $VersionName" 38 | 39 | [xml] $manifest = Get-Content -Path $ManifestPath 40 | 41 | function Select-ManifestAttribute { 42 | param 43 | ( [Parameter(Mandatory)] [xml] $manifestDoc 44 | , [Parameter(Mandatory)] [string] $attributeName 45 | ) 46 | $namespaces = @{android="http://schemas.android.com/apk/res/android"} 47 | $commonXpath = "/manifest/@android:" 48 | return Select-Xml -xml $manifestDoc -Xpath "$commonXpath$attributeName" -namespace $namespaces 49 | } 50 | 51 | $manifestVersionCode = Select-ManifestAttribute $manifest "versionCode" 52 | $manifestVersionName = Select-ManifestAttribute $manifest "versionName" 53 | 54 | Write-Output "OLD MANIFEST VALUES ------------" 55 | Write-Output "Old version code: $($manifestVersionCode.Node.Value)" 56 | Write-Output "Old version name: $($manifestVersionName.Node.Value)" 57 | 58 | if($PrintFile) 59 | { 60 | Write-Output "ORIGINAL MANIFEST --------------" 61 | Get-Content $ManifestPath | Write-Output 62 | } 63 | 64 | $manifestVersionCode.Node.Value = $versionCode 65 | $manifestVersionName.Node.Value = $VersionName 66 | $manifest.Save($ManifestPath) 67 | 68 | if($PrintFile) 69 | { 70 | Write-Output "FINAL MANIFEST -----------------" 71 | Get-Content $ManifestPath | Write-Output 72 | } 73 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/AzureDevOps/example.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/AzureDevOps/example.keystore -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/AzureDevOps/example.keystore.README.txt: -------------------------------------------------------------------------------- 1 | The accompanying example.keystore file was generated by the automatic process of 2 | Xamarin making a keystore when you do a debug-deploy. Xamarin.UITest puts a 3 | test package on the target device/emulator; the app and the package have to 4 | be signed by the same key, so if you are testing an app that is already on 5 | the device/emulator, you can specify the keystore that was used. 6 | 7 | keystore password: android 8 | key alias: androiddebugkey 9 | key password: android 10 | 11 | Read the repo README.md for info on how to use example.keystore in your pipeline. 12 | 13 | Reminder: normally, you don't want to put your keystore or keystore-related 14 | passwords in your repo, but this is a demo repo. 15 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/AzureDevOps/pipeline-android.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | # think about using batching to cut down on overlapping runs 3 | #batch: true 4 | branches: 5 | include: 6 | - main 7 | paths: 8 | include: 9 | # common 10 | - 'XamarinPipelineDemo' 11 | - 'XamarinPipelineDemo.NUnit' 12 | - 'XamarinPipelineDemo.UITest' 13 | - 'XamarinPipelineDemo.sln' 14 | # platform 15 | - 'XamarinPipelineDemo.Android' 16 | 17 | pool: 18 | vmImage: 'macos-latest' 19 | 20 | variables: 21 | - group: android_demo_var_group 22 | 23 | - name: appName 24 | value: 'XamarinPipelineDemo' 25 | 26 | - name: solution 27 | value: '$(Build.SourcesDirectory)/$(appName).sln' 28 | 29 | - name: buildConfiguration 30 | value: 'Release' 31 | 32 | - name: outputDir 33 | value: '$(Build.BinariesDirectory)/$(buildConfiguration)' 34 | 35 | - name: pipelineBuildNumber 36 | value: $[counter(variables['Build.DefinitionName'], 1)] 37 | 38 | - name: finalApkCommonPart 39 | value: '$(Build.ArtifactStagingDirectory)/$(appName)_$(Build.BuildNumber)_$(pipelineBuildNumber)' 40 | 41 | - name: finalApkPathUnsigned 42 | value: '$(finalApkCommonPart)_Unsigned.apk' 43 | 44 | - name: finalApkPathSigned 45 | value: '$(finalApkCommonPart)_Signed.apk' 46 | 47 | - name: androidDir 48 | value: '$(Build.SourcesDirectory)/$(appName).Android' 49 | 50 | - name: androidPipelineDir 51 | value: '$(androidDir)/AzureDevOps' 52 | 53 | - name: nugetPackageDir 54 | value: '$(System.DefaultWorkingDirectory)/packages' 55 | 56 | - name: uiTestDir 57 | value: '$(Build.SourcesDirectory)/$(appName).UITest' 58 | 59 | - name: uiTestAssemblyDir 60 | value: '$(uiTestDir)/bin/$(buildConfiguration)' 61 | 62 | - name: uiTestResultPath 63 | value: '$(Build.ArtifactStagingDirectory)/uitest_result.xml' 64 | 65 | - name: uiTestLogPath 66 | value: '$(Build.ArtifactStagingDirectory)/uitest.log' 67 | 68 | - name: adb 69 | value: '$ANDROID_HOME/platform-tools/adb' 70 | 71 | - name: emulator 72 | value: '$ANDROID_HOME/emulator/emulator' 73 | 74 | - name: appCenterOutputDir 75 | value: '$(Build.ArtifactStagingDirectory)/AppCenterTest' 76 | 77 | - name: appCenterTestResultsDir 78 | value: '$(appCenterOutputDir)/TestResults' 79 | 80 | - name: appCenterOrgName 81 | value: 'JacobEgnerDemos' 82 | 83 | - name: wantAppCenterUiTests 84 | value: true 85 | 86 | - name: wantEmulatorUiTests 87 | value: true 88 | 89 | # set this to false when things are working and you don't need to debug 90 | # pipeline problems 91 | - name: System.Debug 92 | value: true 93 | 94 | ################################################################################ 95 | steps: 96 | 97 | ################################################################################ 98 | # build app and make APK 99 | 100 | # doing a new Xamarin Android app pipeline gets you this task 101 | - task: NuGetToolInstaller@1 102 | 103 | # doing a new Xamarin Android app pipeline gets you a simpler form of this task 104 | - task: NuGetCommand@2 105 | displayName: 'nuget-restore solution' 106 | inputs: 107 | restoreSolution: '$(solution)' 108 | 109 | - task: PowerShell@2 110 | displayName: 'Set build-based Android app version' 111 | inputs: 112 | filePath: '$(androidPipelineDir)/AndroidSetVersion.ps1' 113 | arguments: > 114 | -manifestPath '$(androidDir)/Properties/AndroidManifest.xml' 115 | -versionCodeBase 0 116 | -versionCodeOffset $(pipelineBuildNumber) 117 | -versionName '$(Build.BuildNumber)' 118 | 119 | - task: XamarinAndroid@1 120 | inputs: 121 | projectFile: '**/*droid*.csproj' 122 | outputDir: '$(outputDir)' 123 | configuration: '$(buildConfiguration)' 124 | 125 | - pwsh: Get-ChildItem -Recurse "$(outputDir)/*.apk" | ForEach-Object { Copy-Item $_ -Destination "$(finalApkPathUnsigned)" } 126 | displayName: 'copy and rename unsigned apk' 127 | 128 | # REMINDER: You need to upload 129 | # XamarinPipelineDemo.Android/AzureDevops/example.keystore as a secure file and 130 | # make android_demo_var_group variable group with the following variables... 131 | # androidKeystoreSecureFileName: example.keystore 132 | # androidKeyAlias: androiddebugkey 133 | # androidKeystorePassword: android 134 | # androidKeyPassword: android 135 | - task: AndroidSigning@3 136 | displayName: 'sign APK with example keystore' 137 | inputs: 138 | apkFiles: '$(outputDir)/*.apk' 139 | apksignerKeystoreFile: '$(androidKeystoreSecureFileName)' 140 | apksignerKeystoreAlias: '$(androidKeyAlias)' 141 | apksignerKeystorePassword: '$(androidKeystorePassword)' 142 | apksignerArguments: '--verbose --out $(finalApkPathSigned)' 143 | 144 | - task: PublishBuildArtifacts@1 145 | displayName: 'publish APK artifacts' 146 | inputs: 147 | artifactName: 'apks' 148 | 149 | ################################################################################ 150 | # unit tests 151 | 152 | # our unit test project is .net core, so "dotnet test" works on it 153 | - task: DotNetCoreCLI@2 154 | displayName: 'unit tests' 155 | inputs: 156 | command: 'test' 157 | projects: '**/*NUnit*.csproj' 158 | configuration: '$(buildConfiguration)' 159 | testRunTitle: 'Unit Tests' 160 | 161 | ################################################################################ 162 | # UI tests, preparatory steps common to App Center and emulator 163 | 164 | - task: MSBuild@1 165 | displayName: 'build ui tests' 166 | condition: > 167 | and( 168 | succeeded(), 169 | or( 170 | eq(variables.wantAppCenterUiTests, true), 171 | eq(variables.wantEmulatorUiTests, true) 172 | ) 173 | ) 174 | inputs: 175 | solution: '**/*UITest*.csproj' 176 | configuration: '$(buildConfiguration)' 177 | 178 | ################################################################################ 179 | # emulator UI tests 180 | # lots of inspiration from https://medium.com/genetec-tech/android-ui-testing-in-azure-devops-81bbe7cea9fd 181 | # older sources of inspiration from... 182 | # https://github.com/janpio/azure-pipelines-android_emulator 183 | # https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/android?view=azure-devops#test-on-the-android-emulator 184 | 185 | - bash: | 186 | set -o xtrace 187 | $ANDROID_HOME/tools/bin/sdkmanager --list 188 | echo "y" | $ANDROID_HOME/tools/bin/sdkmanager --install 'system-images;android-30;google_apis;x86' 189 | displayName: 'install Android image' 190 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 191 | 192 | - bash: | 193 | set -o xtrace 194 | $(emulator) -list-avds 195 | echo "no" | $ANDROID_HOME/tools/bin/avdmanager create avd -n uitest_android_emulator -k 'system-images;android-30;google_apis;x86' --force 196 | $(emulator) -list-avds 197 | displayName: 'create AVD' 198 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 199 | 200 | - bash: | 201 | set -o xtrace 202 | $(adb) devices 203 | nohup $(emulator) -avd uitest_android_emulator -no-snapshot -no-boot-anim -gpu auto -qemu > /dev/null 2>&1 & 204 | displayName: 'start Android emulator' 205 | condition: and(succeededOrFailed(), eq(variables.wantEmulatorUiTests, true)) 206 | 207 | - bash: | 208 | set -o xtrace 209 | $(adb) wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82' 210 | $(adb) devices 211 | displayName: 'wait for Android emulator' 212 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 213 | timeoutInMinutes: 5 214 | 215 | # have to run nunit3-console directly; as of 2020-12-17, even though "dotnet 216 | # run" works with Mono to run .net framework projects, "dotnet test" does not 217 | # work the same project; see open issue https://github.com/mono/mono/issues/6984 218 | - pwsh: | 219 | Set-PSDebug -Trace 1 220 | $env:UITEST_APK_PATH = "$(finalApkPathSigned)" 221 | $testAssemblies = Get-Item "$(uiTestDir)/bin/$(buildConfiguration)/$(appName).UITest*.dll" 222 | nunit3-console $testAssemblies --output="$(uiTestLogPath)" --result="$(uiTestResultPath)" 223 | displayName: 'run android emulator ui tests' 224 | condition: and(succeeded(), eq(variables.wantEmulatorUiTests, true)) 225 | continueOnError: true 226 | timeoutInMinutes: 120 227 | 228 | - task: PublishBuildArtifacts@1 229 | displayName: 'publish emulator ui test log artifact' 230 | condition: and(succeededOrFailed(), eq(variables.wantEmulatorUiTests, true)) 231 | continueOnError: true 232 | inputs: 233 | artifactName: 'Android emulator UI test log' 234 | pathToPublish: '$(uiTestLogPath)' 235 | 236 | - task: PublishTestResults@2 237 | condition: and(succeededOrFailed(), eq(variables.wantEmulatorUiTests, true)) 238 | inputs: 239 | testRunTitle: 'Android Emulator UI Test Run' 240 | testResultsFormat: 'NUnit' 241 | testResultsFiles: '$(uiTestResultPath)' 242 | # Android tests may randomly fail because of the System UI not responding (if you're using Prism); 243 | # see https://github.com/PrismLibrary/Prism/issues/2099 ; 244 | # tests may also fail due to pool agent problems; 245 | # using the following line still makes builds have warning status when UI tests fail 246 | failTaskOnFailedTests: false 247 | 248 | ################################################################################ 249 | # AppCenter UI tests 250 | 251 | # default nodejs version (v12) is not compatible with stuff used in AppCenterTest task 252 | # https://github.com/microsoft/appcenter-cli/issues/696#issuecomment-553218361 253 | - task: UseNode@1 254 | displayName: 'Use Node 10.15.1' 255 | condition: and(succeeded(), eq(variables.wantAppCenterUiTests, true)) 256 | inputs: 257 | version: 10.15.1 258 | 259 | - task: AppCenterTest@1 260 | condition: and(succeeded(), eq(variables.wantAppCenterUiTests, true)) 261 | continueOnError: true 262 | inputs: 263 | appFile: '$(finalApkPathSigned)' 264 | appSlug: '$(appCenterOrgName)/$(appName)' # orgname or username, then '/', then app name 265 | devices: '$(appCenterOrgName)/demo_device_set' # uses same orgname or username, then '/', then device set name 266 | frameworkOption: 'uitest' 267 | runOptions: --test-output-dir "$(appCenterOutputDir)" # only needed if publishing test results in Azure DevOps 268 | serverEndpoint: 'AppCenterConnectionUserBasedFullAccess' # make a App Center user API token, then add service connection in Azure DevOps 269 | uiTestBuildDirectory: '$(uiTestAssemblyDir)' # directory that contains the uitest assemblies, not the build directory 270 | uiTestToolsDirectory: '$(uiTestAssemblyDir)' # build process puts test-cloud.exe in assembly dir 271 | 272 | - pwsh: Expand-Archive "$(appCenterOutputDir)/nunit_xml_zip.zip" -DestinationPath "$(appCenterTestResultsDir)" 273 | displayName: 'unzip App Center test results zip' 274 | condition: and(succeededOrFailed(), eq(variables.wantAppCenterUiTests, true)) 275 | continueOnError: true 276 | 277 | - task: PublishTestResults@2 278 | displayName: 'simple-publish App Center UI test results' 279 | condition: and(succeededOrFailed(), eq(variables.wantAppCenterUiTests, true)) 280 | inputs: 281 | testRunTitle: 'Android App Center UI Test Run (simple publish)' 282 | testResultsFormat: 'NUnit' 283 | testResultsFiles: '$(appCenterTestResultsDir)/*.xml' 284 | 285 | - pwsh: | 286 | Get-ChildItem "$(appCenterTestResultsDir)/*.xml" | ForEach-Object { Write-Output ` 287 | ( "##vso[results.publish " ` 288 | + "runTitle=Android App Center UI Test Run $($_.BaseName);" ` 289 | + "resultFiles=$($_.FullName);" ` 290 | + "type=NUnit;" ` 291 | + "mergeResults=false;" ` 292 | + "publishRunAttachments=true;" ` 293 | + "failTaskOnFailedTests=false;" ` 294 | + "testRunSystem=VSTS - PTR;" ` 295 | + "]" ` 296 | )} 297 | displayName: 'complicated-publish App Center UI test results with device name' 298 | condition: and(succeededOrFailed(), eq(variables.wantAppCenterUiTests, true)) 299 | 300 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Android.App; 4 | using Android.Content.PM; 5 | using Android.Runtime; 6 | using Android.Views; 7 | using Android.Widget; 8 | using Android.OS; 9 | 10 | namespace XamarinPipelineDemo.Droid 11 | { 12 | [Activity(Label = "XamarinPipelineDemo", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize )] 13 | public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity 14 | { 15 | protected override void OnCreate(Bundle savedInstanceState) 16 | { 17 | TabLayoutResource = Resource.Layout.Tabbar; 18 | ToolbarResource = Resource.Layout.Toolbar; 19 | 20 | base.OnCreate(savedInstanceState); 21 | 22 | Xamarin.Essentials.Platform.Init(this, savedInstanceState); 23 | global::Xamarin.Forms.Forms.Init(this, savedInstanceState); 24 | LoadApplication(new App()); 25 | } 26 | public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) 27 | { 28 | Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); 29 | 30 | base.OnRequestPermissionsResult(requestCode, permissions, grantResults); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using Android.App; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("XamarinPipelineDemo.Android")] 10 | [assembly: AssemblyDescription("")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("")] 13 | [assembly: AssemblyProduct("XamarinPipelineDemo.Android")] 14 | [assembly: AssemblyCopyright("Copyright © 2014")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | [assembly: ComVisible(false)] 18 | 19 | // Version information for an assembly consists of the following four values: 20 | // 21 | // Major Version 22 | // Minor Version 23 | // Build Number 24 | // Revision 25 | [assembly: AssemblyVersion("1.0.0.0")] 26 | [assembly: AssemblyFileVersion("1.0.0.0")] 27 | 28 | // Add some common permissions, these can be removed if not needed 29 | [assembly: UsesPermission(Android.Manifest.Permission.Internet)] 30 | [assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)] 31 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/AboutResources.txt: -------------------------------------------------------------------------------- 1 | Images, layout descriptions, binary blobs and string dictionaries can be included 2 | in your application as resource files. Various Android APIs are designed to 3 | operate on the resource IDs instead of dealing with images, strings or binary blobs 4 | directly. 5 | 6 | For example, a sample Android app that contains a user interface layout (main.xml), 7 | an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) 8 | would keep its resources in the "Resources" directory of the application: 9 | 10 | Resources/ 11 | drawable-hdpi/ 12 | icon.png 13 | 14 | drawable-ldpi/ 15 | icon.png 16 | 17 | drawable-mdpi/ 18 | icon.png 19 | 20 | layout/ 21 | main.xml 22 | 23 | values/ 24 | strings.xml 25 | 26 | In order to get the build system to recognize Android resources, set the build action to 27 | "AndroidResource". The native Android APIs do not operate directly with filenames, but 28 | instead operate on resource IDs. When you compile an Android application that uses resources, 29 | the build system will package the resources for distribution and generate a class called 30 | "Resource" that contains the tokens for each one of the resources included. For example, 31 | for the above Resources layout, this is what the Resource class would expose: 32 | 33 | public class Resource { 34 | public class drawable { 35 | public const int icon = 0x123; 36 | } 37 | 38 | public class layout { 39 | public const int main = 0x456; 40 | } 41 | 42 | public class strings { 43 | public const int first_string = 0xabc; 44 | public const int second_string = 0xbcd; 45 | } 46 | } 47 | 48 | You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main 49 | to reference the layout/main.xml file, or Resource.strings.first_string to reference the first 50 | string in the dictionary file values/strings.xml. 51 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/layout/Tabbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/layout/Toolbar.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-anydpi-v26/icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-anydpi-v26/icon_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-hdpi/icon.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-hdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-hdpi/launcher_foreground.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-mdpi/icon.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-mdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-mdpi/launcher_foreground.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-xhdpi/icon.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-xhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-xhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-xxhdpi/icon.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-xxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-xxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-xxxhdpi/icon.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/mipmap-xxxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmegner/XamarinPipelineDemo/9d70dececb817b2eb42082971617ce7fb3f16a02/XamarinPipelineDemo.Android/Resources/mipmap-xxxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | #3F51B5 5 | #303F9F 6 | #FF4081 7 | 8 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.Android/XamarinPipelineDemo.Android.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | {B238D9EF-BAA5-441D-84CC-8B8A37128BBB} 7 | {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 8 | {c9e5eea5-ca05-42a1-839b-61506e0a37df} 9 | Library 10 | XamarinPipelineDemo.Droid 11 | XamarinPipelineDemo.Android 12 | True 13 | True 14 | Resources\Resource.designer.cs 15 | Resource 16 | Properties\AndroidManifest.xml 17 | Resources 18 | Assets 19 | false 20 | v9.0 21 | true 22 | true 23 | 24 | 25 | 26 | 27 | true 28 | full 29 | false 30 | bin\Debug 31 | DEBUG; 32 | prompt 33 | 4 34 | None 35 | false 36 | false 37 | false 38 | false 39 | 40 | 41 | false 42 | pdbonly 43 | true 44 | bin\Release 45 | prompt 46 | 4 47 | true 48 | false 49 | false 50 | false 51 | true 52 | false 53 | 54 | armeabi-v7a;x86;arm64-v8a 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 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | {552478C4-010A-4643-8ACC-0964FB460D1D} 106 | XamarinPipelineDemo 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.NUnit/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace XamarinPipelineDemo.NUnit 4 | { 5 | public class Tests 6 | { 7 | [SetUp] 8 | public void Setup() 9 | { 10 | } 11 | 12 | [Test] 13 | public void NUnitTest1() 14 | { 15 | Assert.Pass(); 16 | } 17 | 18 | [Test] 19 | public void NUnitTest2() 20 | { 21 | Assert.Pass(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /XamarinPipelineDemo.NUnit/XamarinPipelineDemo.NUnit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.UITest/AppInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Xamarin.UITest; 6 | using Xamarin.UITest.Configuration; 7 | using Xamarin.UITest.Utils; 8 | 9 | [assembly: System.Reflection.AssemblyVersionAttribute("0.0.0.0")] 10 | namespace XamarinPipelineDemo.UITest 11 | { 12 | public static class AppInitializer 13 | { 14 | public static IApp StartApp(Platform platform) 15 | { 16 | if (platform == Platform.Android) 17 | { 18 | return ConfigureApp.Android 19 | .Debug() 20 | .EnableLocalScreenshots() 21 | .ApkFileFromEnvironmentOrPreinstalledApp("com.demo.XamarinPipelineDemo") 22 | .StartApp(); 23 | } 24 | 25 | return ConfigureApp.iOS 26 | .EnableLocalScreenshots() 27 | .StartApp(); 28 | } 29 | 30 | private static AndroidAppConfigurator ApkFileFromEnvironmentOrPreinstalledApp( 31 | this AndroidAppConfigurator app, 32 | string preinstalledAppName) 33 | { 34 | var envApkPath = "UITEST_APK_PATH"; 35 | var envKeystorePath = "UITEST_KEYSTORE_PATH"; 36 | var envKeystorePassword = "UITEST_KEYSTORE_PASSWORD"; 37 | var envKeyAlias = "UITEST_KEY_ALIAS"; 38 | var envKeyPassword = "UITEST_KEY_PASSWORD"; 39 | var allKeystoreEnvs = new[] { envKeystorePath, envKeystorePassword, envKeyAlias, envKeyPassword }; 40 | var allEnvs = allKeystoreEnvs.Concat(new[] { envApkPath }).ToArray(); 41 | 42 | var envDict = allEnvs.ToDictionary(envName => envName, envName => Environment.GetEnvironmentVariable(envName)); 43 | 44 | foreach(var entry in envDict) 45 | { 46 | Console.WriteLine($"DEMO_NOTE: envDict key='{entry.Key}' value='{entry.Value}'"); 47 | } 48 | 49 | if(!string.IsNullOrWhiteSpace(envDict[envApkPath])) 50 | { 51 | Console.WriteLine($"DEMO_NOTE: using apk file"); 52 | app = app.ApkFile(envDict[envApkPath]); 53 | } 54 | else 55 | { 56 | Console.WriteLine($"DEMO_NOTE: using preinstalled app name"); 57 | app = app.InstalledApp(preinstalledAppName); 58 | 59 | if(allKeystoreEnvs.All(envName => !string.IsNullOrWhiteSpace(envDict[envName]))) 60 | { 61 | Console.WriteLine($"DEMO_NOTE: using keystore"); 62 | app = app.KeyStore( 63 | envDict[envKeystorePath], 64 | envDict[envKeystorePassword], 65 | envDict[envKeyPassword], 66 | envDict[envKeyAlias]); 67 | } 68 | } 69 | 70 | return app; 71 | } 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.UITest/Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | using Xamarin.UITest; 6 | using Xamarin.UITest.Queries; 7 | 8 | namespace XamarinPipelineDemo.UITest 9 | { 10 | [TestFixture(Platform.Android)] 11 | //[TestFixture(Platform.iOS)] 12 | public class Tests 13 | { 14 | IApp _app; 15 | Platform _platform; 16 | 17 | public Tests(Platform platform) 18 | { 19 | this._platform = platform; 20 | } 21 | 22 | [SetUp] 23 | public void BeforeEachTest() 24 | { 25 | _app = AppInitializer.StartApp(_platform); 26 | } 27 | 28 | [Test] 29 | public void FoundLabel() 30 | { 31 | var labelResults = _app.WaitForElement("Some text."); 32 | Assert.That(labelResults.Count() == 1); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.UITest/XamarinPipelineDemo.UITest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | {F52E5C1F-96F9-445C-B2C9-1ADC2418D71E} 7 | Library 8 | XamarinPipelineDemo.UITest 9 | XamarinPipelineDemo.UITest 10 | v4.6.1 11 | 12 | 13 | true 14 | full 15 | false 16 | bin\Debug 17 | DEBUG; 18 | prompt 19 | 4 20 | 21 | 22 | true 23 | bin\Release 24 | prompt 25 | 4 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /XamarinPipelineDemo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30804.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamarinPipelineDemo.Android", "XamarinPipelineDemo.Android\XamarinPipelineDemo.Android.csproj", "{B238D9EF-BAA5-441D-84CC-8B8A37128BBB}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamarinPipelineDemo", "XamarinPipelineDemo\XamarinPipelineDemo.csproj", "{6A6F6A44-34C6-4DFD-B755-B2AC0CCD5EB5}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamarinPipelineDemo.NUnit", "XamarinPipelineDemo.NUnit\XamarinPipelineDemo.NUnit.csproj", "{3CE6C2C8-4386-400E-83FF-AEFA7F4C90ED}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamarinPipelineDemo.UITest", "XamarinPipelineDemo.UITest\XamarinPipelineDemo.UITest.csproj", "{F52E5C1F-96F9-445C-B2C9-1ADC2418D71E}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LocalScripts", "LocalScripts", "{8FF8048C-F80E-4006-9A7A-D1B33AFDA42B}" 15 | ProjectSection(SolutionItems) = preProject 16 | LocalScripts\appcenter_uitest_run.ps1 = LocalScripts\appcenter_uitest_run.ps1 17 | LocalScripts\common.ps1 = LocalScripts\common.ps1 18 | LocalScripts\local_uitest_run.ps1 = LocalScripts\local_uitest_run.ps1 19 | EndProjectSection 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{0723ED59-10D3-443C-AF14-281AE07FC8AC}" 22 | ProjectSection(SolutionItems) = preProject 23 | README.md = README.md 24 | EndProjectSection 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {B238D9EF-BAA5-441D-84CC-8B8A37128BBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {B238D9EF-BAA5-441D-84CC-8B8A37128BBB}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {B238D9EF-BAA5-441D-84CC-8B8A37128BBB}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 35 | {B238D9EF-BAA5-441D-84CC-8B8A37128BBB}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {B238D9EF-BAA5-441D-84CC-8B8A37128BBB}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {B238D9EF-BAA5-441D-84CC-8B8A37128BBB}.Release|Any CPU.Deploy.0 = Release|Any CPU 38 | {6A6F6A44-34C6-4DFD-B755-B2AC0CCD5EB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {6A6F6A44-34C6-4DFD-B755-B2AC0CCD5EB5}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {6A6F6A44-34C6-4DFD-B755-B2AC0CCD5EB5}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 41 | {6A6F6A44-34C6-4DFD-B755-B2AC0CCD5EB5}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {6A6F6A44-34C6-4DFD-B755-B2AC0CCD5EB5}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {6A6F6A44-34C6-4DFD-B755-B2AC0CCD5EB5}.Release|Any CPU.Deploy.0 = Release|Any CPU 44 | {3CE6C2C8-4386-400E-83FF-AEFA7F4C90ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {3CE6C2C8-4386-400E-83FF-AEFA7F4C90ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {3CE6C2C8-4386-400E-83FF-AEFA7F4C90ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {3CE6C2C8-4386-400E-83FF-AEFA7F4C90ED}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {F52E5C1F-96F9-445C-B2C9-1ADC2418D71E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {F52E5C1F-96F9-445C-B2C9-1ADC2418D71E}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {F52E5C1F-96F9-445C-B2C9-1ADC2418D71E}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {F52E5C1F-96F9-445C-B2C9-1ADC2418D71E}.Release|Any CPU.Build.0 = Release|Any CPU 52 | EndGlobalSection 53 | GlobalSection(SolutionProperties) = preSolution 54 | HideSolutionNode = FALSE 55 | EndGlobalSection 56 | GlobalSection(ExtensibilityGlobals) = postSolution 57 | SolutionGuid = {C2B89E11-112C-4F8C-900D-BD6062226E8A} 58 | EndGlobalSection 59 | EndGlobal 60 | -------------------------------------------------------------------------------- /XamarinPipelineDemo/App.xaml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XamarinPipelineDemo/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xamarin.Forms; 3 | using Xamarin.Forms.Xaml; 4 | 5 | namespace XamarinPipelineDemo 6 | { 7 | public partial class App : Application 8 | { 9 | public App() 10 | { 11 | InitializeComponent(); 12 | 13 | MainPage = new MainPage(); 14 | } 15 | 16 | protected override void OnStart() 17 | { 18 | } 19 | 20 | protected override void OnSleep() 21 | { 22 | } 23 | 24 | protected override void OnResume() 25 | { 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /XamarinPipelineDemo/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms.Xaml; 2 | 3 | [assembly: XamlCompilation(XamlCompilationOptions.Compile)] -------------------------------------------------------------------------------- /XamarinPipelineDemo/MainPage.xaml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /XamarinPipelineDemo/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xamarin.Forms; 8 | 9 | namespace XamarinPipelineDemo 10 | { 11 | public partial class MainPage : ContentPage 12 | { 13 | public MainPage() 14 | { 15 | InitializeComponent(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /XamarinPipelineDemo/XamarinPipelineDemo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | 7 | 8 | 9 | portable 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------