[Azure DevOps Pipelines] c#(.NET Framework) を ビルドする

今更ながら Azure DevOps 環境で c# (.NET Framework) アプリを CI/CD してみたいと思いまして、トライした内容をこちらで記載してみたいと思います。

 

 

 

1. 環境準備

 

1.1. "Azure DevOps" に Git プロジェクトを作成

"Azure DevOps" のアカウントを既に所有しているものとして記載します。

 

Step 1

[+ New Project]ボタンをクリックします。

+ New Project

 

Step 2

"Procjet name" など必要事項を記入および選択後、[Create]ボタンをクリックします。

私はこの後で超簡単な Counter プログラムを作成してリポジトリへ登録する予定なので、"Project name" を Counter として作成します。

Create New Project

 

下図はプロジェクト作成完了直後の画面です。

プロジェクト作成直後の画面

 

 

1.2. c# (.NET Framework 4.8) プロジェクトを作成

Pipeline を実行する対象となるプロジェクトを作成します。

ここでは以下のようなテストプロジェクトを作成して進めました。

 

Step 1

先ほど作成した Git プロジェクト(Counter)を Clone します。

下図あたりの表示にあるコマンドを使ったり、画面上のボタン操作を行うことで git clone します。

git clone

 

Step 2

Clone した場所へ Visual Studio で新しい c# プロジェクトを作成します。

私は以下のような環境で c# プロジェクトを作成しました。

 

[環境]

コンパイラ : Visual Studio 2022, Version 17.8.4
開発言語: c#  
フレームワーク: .NET Framework 4.8  
テストフレームワーク: MSTest.TestAdapter, 3.1.1
  MSTest.TestFramework, 3.1.1
  NUnit, 4.0.1
  NUnit3TestAdapter, 4.5.0
OS : Windows11 home, 23H2

 

重要

上記「テストフレームワーク」に記載の4つは、必ず NuGet で登録を行います。

NuGet 以外の方法で環境に登録すると、後述の Pipelines で期待する動作をすることが難しくなります。

 

NuGet パッケージ

 

代表的な Unit テストフレームワーク(NUnit、MSTestV1、MSTestV2)についても準備して動作を評価してみます。

 

[table.] 各プロジェクトの説明
プロジェクト名 内容
CounterDll 値(Integer)を管理する class Counter を提供する dll。
class Couter は、Increment, Decrement, Clear 3つのメソッドを持つ。
CounterForm class Counter を使った WinForm アプリ。
CounterWPF class Counter を使った WPF アプリ。
testCounter1 CounterDll をテストする。NUnt 版。
testCounter2 CounterDll をテストする。MSTestV1 版。
testCounter3 CounterDll をテストする。MSTestV2 版。

 

Visual Studio 上の画面の様子です。

Visual Studio

 

図: CounterForm 画面例

CounterForm

 

図: CounterWPF 画面例

CounterWPF

 

 

1.3. Git リポジトリへプロジェクトをコミット&プッシュ

Git (Azure DevOps) へコミット&プッシュします。手順については本ページの主たる内容から外れるので記載を割愛します。

コミット&プッシュ後の Azure DevOps 画面です。

Azure DevOps 画面

 

以上で Pipelines を操作する前の事前準備を完了です。

 


 

2. Pipeline を作成

Pipeline を作成していきます。

 

Step 1

画面左側の Pipeline ボタンをクリックすると下図のような画面を表示するので、[Create Pipeline]ボタンをクリックします。

Pipeline

 

Step 2

Pipeline の対象となるソースコードの所在を選択します。本ページでは "Azure Repos Git" を選択します。

Where is your code?

 

Step 3

対象リポジトリを選択します。ここでは Counter を選択します。

Select repository

 

Step 4

Pipeline を構成するプラットフォームを選択します。ここでは「.NET Desktop」を選択します。

Configure your pipeline

 

Step 5

"azure-pipelines.yml" ファイルを生成してリポジトリへコミットされました。

内容を確認して必要に応じて修正します。まずは内容を確認してみましょう。

Review your pipeline YAML

 

生成された yml ファイルのコードは以下のとおりです。

(1)NuGet パッケージの復元して、(2)ビルドして、(3)ユニットテストする、というような流れです。

 

["azure-pipelines.yml"]

# .NET Desktop 
# Build and run tests for .NET Desktop or Windows classic desktop solutions. 
# Add steps that publish symbols, save build artifacts, and more: 
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger: 
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps: 
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

 

一部について説明します。

要素 内容 備考
trigger CI トリガーを取得するブランチを設定します。
vmImage ビルドするプラットフォームを設定します。Microsoft によってホストされるエージェントから指定します。

今回の例は VS2022 を使っているので 'windows-2022' を指定してもOKです。

NuGetToolInstaller@1 インターネットまたはツール キャッシュから特定のバージョンの NuGet を取得し、PATH に追加します。 このタスクを使用して、NuGet タスクで使用される NuGet のバージョンを変更します。
NuGetCommand@2 このタスクを使用して、NuGet パッケージの復元、パック、プッシュ、または NuGet コマンドの実行を行います。
VSBuild@1 このタスクを使用して MSBuild を使用してビルドし、Visual Studio バージョン プロパティを設定します。
VSTest@2 このタスクを使用して、Visual Studio テスト (VSTest) ランナーを使用して単体テストと機能テスト (Selenium、Appium、コード化された UI テストなど) を実行します。 Visual Studio テスト アダプターを持つテスト フレームワークを実行できます。
フレームワークの例としては、MSTest、xUnit、NUnit、Chutzpah (QUnit、Mocha、Jasmine を使用する JavaScript テスト用) などがあります。

 

Step 6

いよいよ[Save and run]してみます。下図[Save and run]ボタンをクリックします。

Save and run

 

下図画面を表示するので、もう一度[Save and run]ボタンをクリックします。

Save and run

 

Pipeline を実行開始しました。

Running jobs

 

Pipeline を正常に完了ました。完了後の画面を以下に示します。

Build succeeded

 

「✔ Job」のあたりをクリックすることで Job 詳細を確認することができます。

どうやら Pipeline は全て正常に処理できたようです。

Jobs in run

 

以上から確認できたとりあえずの課題を以下に列挙します。

課題:

  1. ビルド生成物(artifacts)を登録できていない。
    「0 artifacts」 という表示がありますが、ちゃんと設定できるとここからビルド生成物などを取得できるようになります。
  2. Unitテストを実施できていない。
    Warnings
    がありますね。どうやら、VSTest の対象を見つけることができなかった、という内容のようです。
    少なくともデフォルト状態では "**\bin\**\*test.dll, **\bin\**\*tests.dll" というファイル名を満足する必要があるようです。

 

次章からこれら課題を順に解決していきます。

 


 

3. ビルド生成物(artifacts)を登録する

ビルド生成物(artifacts)を登録するように yml ファイルを修正します。

使用するコマンドは下記の2つです。

CopyFiles@2 で登録したいファイルを一か所に集めて、PublishBuildArtifacts@1 でアーティファクト登録する、というような手順です。

 

他:

 

要素 内容 備考
CopyFiles@2 一致パターンを使用してソース フォルダーからターゲット フォルダーにファイルをコピーします。 (一致パターンは、フォルダー パスではなく、ファイル パスにのみ一致します)。
PublishBuildArtifacts@1 ビルド パイプラインでこのタスクを使用して、ビルド成果物を Azure Pipelines、TFS、またはファイル共有に発行します。

 

["azure-pipelines.yml"]

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: CopyFiles@2
  inputs:
    contents: CounterWPF\bin\$(buildConfiguration)\**\!(*.pdb)
    targetFolder: $(Build.ArtifactStagingDirectory)

- task: CopyFiles@2
  inputs:
    contents: |
      CounterForm\bin\$(buildConfiguration)\*.*
      !**\*.pdb
    targetFolder: $(Build.ArtifactStagingDirectory)

- task: PublishBuildArtifacts@1
  inputs:
    pathToPublish: $(Build.ArtifactStagingDirectory)
    artifactName: 'MyBuildOutputs'
    publishLocation: 'Container'

 

Pipeline 実行完了後の画面です。「1 published」に変わりました。

Summary

 

1 published」あたりの画面をクリックすると下図 Artifacts 画面を表示します。

[・・・] ボタンウォリックすることであなたが希望する Artifacts をダウンロードなどすることができます。

Artifacts 

 

 


 

4. ビルド生成物(artifacts)を zip 圧縮して登録する

前章では Artifacts として各ファイルを保存する例を紹介しました。しかしながら実際の運用ではアプリケーションは多くのファイルから構成されており、個別ファイルよりも必要となるファイル一式を zip などで圧縮したものを Artifacts として登録する方が都合が良い場合が多いと考えます。

本章では、事前に zip 圧縮したビルド生成物(artifacts)を登録するように yml ファイルを修正します。

使用するコマンドは下記の2つです。

ArchiveFiles@2 でフォルダをまとめて zip 圧縮して、PublishBuildArtifacts@1 でアーティファクト登録する、というような手順です。

 

他:

 

要素 内容 備考
ArchiveFiles@2 .7z、.tar、.gz、.zipなどの圧縮形式を使用してファイルをアーカイブします。
PublishBuildArtifacts@1 ビルド パイプラインでこのタスクを使用して、ビルド成果物を Azure Pipelines、TFS、またはファイル共有に発行します。

 

["azure-pipelines.yml"]

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterWPF\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterForm\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

 

Pipelines 実行完了後の Artifacts 画面の様子は以下の通りです。

Artifacts

 

 


 

5. Unit テストを実行する

Unitテストを実行できるようにします。

現在の Warnings メッセージを見るとテスト用 dll のファイル名を変更するだけでもある程度動作しそうですが、任意のファイル名でテストできる方が今後の応用のためにも良いでしょう。そんな方向で進めてみます。

 

VSTest Warnings

 

具体的には、VSTest@2inputs にて testAssemblyVer2 という要素でテストアセンブリを具合的に指定することで解決できました。
詳細は下記例を参照ください。

要素 内容 備考
VSTest@2 このタスクを使用して、Visual Studio テスト (VSTest) ランナーを使用して単体テストと機能テスト (Selenium、Appium、コード化された UI テストなど) を実行します。 Visual Studio テスト アダプターを持つテスト フレームワークを実行できます。
フレームワークの例としては、MSTest、xUnit、NUnit、Chutzpah (QUnit、Mocha、Jasmine を使用する JavaScript テスト用) などがあります。

 

["azure-pipelines.yml"]

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    testAssemblyVer2: |
      **\$(BuildConfiguration)\testCounter1.dll
      **\$(BuildConfiguration)\testCounter2.dll
      **\$(BuildConfiguration)\testCounter3.dll
      !**\obj\**
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterWPF\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterForm\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

 

以下、Pipelines 実行後の画面です。

Warnings が消えました。

Summary

 

16個のテストを実行して全てをパスしたことがわかります。

VSTest@2 で NUnit, MSTestV1, MSTestV2 を全部実行してくれたことがうれしい感じでした。

Tests

 

"Code Coverage" は空でした。

Code Coverage

 

次章ではこの "Code Coverage" を有効化してみたいと思います。

 

参考

VSTest@2 は恐らく "vstest.console.exe" だと思われます。
ローカルPC上のコンソールにて下記コマンドにより同様に16個のテストを実行して同じ結果を得らえました。

vstest.console.exe ./testCounter*/bin/debug/testCounter*.dll

 


 

6. カバレッジ計測

コード カバレッジは、ソースがどれくらいテストされているかを確認するのに役立つ指標および手法やツールのことを意味します。

本章では、前章で空だった "Code Coverage" タブで確認結果を表示できるようになるようにします。

 

Step 1

早速 VSTest@2 実施時にカバレッジ計測を追加してみましょう。VSTest@2 に「codeCoverageEnabled: true」の1行を追加するだけです。

 

["azure-pipelines.yml"]

  # .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    testAssemblyVer2: |
      **\$(BuildConfiguration)\testCounter1.dll
      **\$(BuildConfiguration)\testCounter2.dll
      **\$(BuildConfiguration)\testCounter3.dll
      !**\obj\**
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    codeCoverageEnabled: true

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterWPF\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterForm\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

 

Pipelines 実行後の「Code Coverage」画面です。

評価対象である CounterDll.dll および関係するアセンブリのコードカバレッジ結果を確認することができるようになりました。

Code Coverage

 

 

Step 2

Step 1 の結果には不要なモジュール「nunit3.testadapter.dll 」のカバレッジ結果も含まれていました。関係はしているので含まれることは正常な動作と思いますが、これを削除したいと思いました。

これを実現する方法を説明したページがありました。

この内容に従って以下の修正を行います。

".runsetting" というファイルを新規に作成し、CodeCoverage の対象から除外したいファイルを <Exclude>~</Exclude> の部分に記載します。ファイル名は別の名称でも問題ありません。

 

[".runsetting"]

<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="Code Coverage">
        <Configuration>
          <CodeCoverage>
            <ModulePaths>
              <Exclude>
                <ModulePath>nunit3.testadapter.dll</ModulePath>
                <!-- Add more ModulePath nodes here. -->
              </Exclude>
            </ModulePaths>
          </CodeCoverage>
        </Configuration>
      </DataCollector>
    </DataCollectors>
  </DataCollectionRunSettings>
</RunSettings>

 

そして、VSTest@2 に「runSettingsFile: '.\.runsettings'」の1行を追加するだけです。

 

["azure-pipelines.yml"]

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    testAssemblyVer2: |
      **\$(BuildConfiguration)\testCounter1.dll
      **\$(BuildConfiguration)\testCounter2.dll
      **\$(BuildConfiguration)\testCounter3.dll
      !**\obj\**
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    codeCoverageEnabled: true
    runSettingsFile: '.\.runsettings'

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterWPF\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterForm\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

 

以上の修正により、不要なモジュールの結果を除外することができました。

Code Coverage

 

 

Step 3

以上の手順により、一応カバレッジを計測して結果を見ることができました。しかし、出力結果は HTML や XML ではなく .coverage 拡張子なので、"Code Coverage" 画面上でソースコードと対比してのカバレッジ確認をすることはできませんでした。結果をダウンロードすることもできますが、カバレッジ計測に対応している "Visual Studio Enterprise" 以外ではどちらにせよ内容を見られない、ということのようです。

ここまで頑張ってみましたが、ソースコードと対比してカバレッジを確認できないのは残念でした。

これを実現する方法について説明している記事がありました。

この内容に従って yml ファイルを以下のように修正します。

 

["azure-pipelines.yml"]

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
  vmImage: 'windows-latest'
  # vmImage: 'windows-2022'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    testAssemblyVer2: |
      **\$(BuildConfiguration)\testCounter1.dll
      **\$(BuildConfiguration)\testCounter2.dll
      **\$(BuildConfiguration)\testCounter3.dll
      !**\obj\**
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    searchFolder: '$(System.DefaultWorkingDirectory)'
    resultsFolder: '$(build.ArtifactStagingDirectory)/Test/Results'
    otherConsoleOptions: '/collect:"Code Coverage;Format=Cobertura"'  # <<<< this is the important bit
    codeCoverageEnabled: true
    runSettingsFile: '.\.runsettings'

# vv Then add publish coverage manually vv
- task: PublishCodeCoverageResults@1
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: '$(build.ArtifactStagingDirectory)/Test/Results/**/*.xml'

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterWPF\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterForm\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

 

上記修正を行った後に実行した Pipelines 結果から "Code Coverage" の部分を以下に表示します。

無事にカバレッジ結果の詳細を閲覧できるようになりました。

Coverage

 

Coverage

 

Coverage

 

"Branch coverage" も計測できており十分活用できそうです。とても良い感じになりました。

 

注意

(後日談として) カバレッジ結果をよく確認してみると、期待するカウント値とどうも違う結果になっていることに気づきました。まだ何かしら設定などで足りないところがある気がします。

後述の「7._OpenCover_でカバレッジ計測」はちゃんと動作してそうなので、当面は OpenCover による方法をご検討ください。

 

 


 

7. OpenCover でカバレッジ計測

OpenCover は、.NET 2 以上(Windows OSのみ - MONOを除く)に対応したコードカバレッジツールです。32ビットと64ビットのプロセスをサポートし、ブランチとシーケンスポイントの両方をカバーします。

本章では OpenCover を使ってカバレッジ計測を行う方法について記載します。

ローカルPCで OpenCover によるカバレッジ計測をできるようになってから、次に Azure の Pipelines で OpenCover できるようにする、という順序で進めてみます。

 

7.1. ローカルPCで OpenCover

最初に NuGet を使って下記2つのパッケージを追加します。

 

NuGet

 

現在の環境を更新して以下に記載します。

 

[環境]

コンパイラ : Visual Studio 2022, Version 17.8.4
開発言語: c#  
フレームワーク: .NET Framework 4.8  
テスト: MSTest.TestAdapter, 3.1.1
  MSTest.TestFramework, 3.1.1
  NUnit, 4.0.1
  NUnit3TestAdapter, 4.5.0
OpenCover 4.7.1221
ReportGenerator 5.2.0
OS : Windows11 home, 23H2

 

"OpenCover.Console.exe" から "vstest.console.exe" を実行してテストを実行し、カバレッジ計測を行います。

そして出力されたカバレッジ計測結果を "ReportGenerator.exe" を使って HTML レポートを作成します。

作成したバッチファイル例を記載します。

 

["OpenCover.bat"]

rem Microsoft Visual Studio 2022 環境設定
call "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"

rem OpenCoverの実行
.\packages\OpenCover.4.7.1221\tools\OpenCover.Console.exe -register:Path64 -target:"vstest.console.exe" -targetargs:"./testCounter*/bin/debug/testCounter*.dll" -targetdir:"." -filter:"+[CounterDll]*" -output:OpenCoverReport.xml

rem OpenCover の出力レポートを整形出力
.\packages\ReportGenerator.5.2.0\tools\net47\ReportGenerator.exe -reports:".\OpenCoverReport.xml" -reporttypes:html -targetdir:".\CoverageReport"

rem レポートを表示
.\CoverageReport\index.html

pause

 

[Table: OpenCover パラメータについて説明]
パラメータ 説明
-target OpenCover が実行するアプリケーションを指定します。今回は "VSTest.console.exe" を指定しています。
-register "Path64" または "Path32" を指定します。この指定に従って "OpenCover.Console.exe" と同じディレクトリにある {x86/x64}\OpenCover.Profile.dll を使い分けるようです。
-targetargs "-target" で指定したアプリケーションに対する引数を指定します。
上記例では "VSTest.console.exe" の引数としてテストプロジェクトの dll ファイルをワイルドカードを使って指定しています。
-targetdir テストプロジェクトの dll があるディレクトリを指定します。ここで指定した場所からの相対パスで扱われる、と思われるので本例では "." を指定してみました。
-returntargetcode これを指定すると、"-target" で指定したアプリケーションからの戻り値を OpenCover の戻り値として扱います。
-output カバレッジ情報を出力する XML ファイル名を指定します。上記例では "OpenCoverReport.xml" というファイル名を指定しています。
-filter カバレッジ計測する名前空間を指定します。
本例では "CounterDll" という名前空間を対象として指定しています。
書式については OpenCover の資料 や様々なインターネット記事を参照してください。

 

[Table: reportgenerator 引数について]
パラメータ 説明
-reports OpenCover が出力した解析結果(XMLファイル)を指定します。
本ページ例では ".\OpenCoverReport.xml" を指定します。
-targetdir 結果を出力するディレクトリを指定します。
本ページ例では ".\CoverageReport" を指定します。
-reporttypes 出力するレポートタイプを指定します。複数指定することが可能です。
本ページ例では "html" を指定します。
指定できるタイプはとてもたくさんあります。こちら を参照。

 

ReportGenerator に関する参照ページ:

 

 

上記バッチファイルを実行した様子の例です。

OpenCover.bat 実行結果

 

出力されたレポート例です。

カバレッジ結果レポート

 

上図のクラス名(赤字部分)をクリックすると、下図のようなカバレッジ詳細を閲覧することができます。

カバレッジ結果レポート

 

 

7.2. "Azure DevOps" で OpenCover

ローカルPCで確認した手順を参考に、今度は "Azure DevOps" の Pipelines 上でコードカバレッジを表示できるようにします。

 

Step 1

VSTest@2 をコメントアウト(または削除)します。

 

["azure-pipelines.yml" (修正箇所のみの略記)]

・・・

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

# - task: VSTest@2
#   inputs:
#     testAssemblyVer2: |
#       **\$(BuildConfiguration)\testCounter1.dll
#       **\$(BuildConfiguration)\testCounter2.dll
#       **\$(BuildConfiguration)\testCounter3.dll
#       !**\obj\**
#     platform: '$(buildPlatform)'
#     configuration: '$(buildConfiguration)'
#     codeCoverageEnabled: true
#     runSettingsFile: '.\.runsettings'

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterWPF\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'

・・・

 

 

Step 2

"VSTest & OpenCover" の処理を追加します。"CmdLine@2" というタスクを使って実現します。

 

["azure-pipelines.yml"]

・・・

- task: CmdLine@2
  displayName: "VSTest & OpenCover"
  inputs:
    script: >
      packages\OpenCover.4.7.1221\tools\OpenCover.Console.exe
      -target:"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe"
      -targetargs:".\testCounter*\bin\$(BuildConfiguration)\testCounter*.dll /logger:trx;LogFileName=TestResult.xml"
      -targetdir:"."
      -register:Path64
      -filter:"+[CounterDll]*"
      -output:"OpenCoverReport.xml"
      -returntargetcode

・・・

 

[Table: OpenCover 引数について]
属性 説明
-target OpenCover が実行するアプリケーションを指定します。今回は "VSTest.console.exe" を指定しています。
-register "Path64" または "Path32" を指定します。この指定に従って "OpenCover.Console.exe" と同じディレクトリにある {x86/x64}\OpenCover.Profile.dll を使い分けるようです。
-targetargs "-target" で指定したアプリケーションに対する引数を指定します。
上記例では "VSTest.console.exe" の引数として下記2つを指示しています。
  • テストプロジェクトの dll ファイルをワイルドカード("*")を使って指定
  • テスト結果を "TestResult.xml" というファイル名で出力するように指定
-targetdir テストプロジェクトの dll があるディレクトリを指定します。ここで指定した場所からの相対パスで扱われる、と思われるので本例では "." を指定してみました。
-returntargetcode これを指定すると、"-target" で指定したアプリケーションからの戻り値を OpenCover の戻り値として扱います。
-output カバレッジ情報を出力する XML ファイル名を指定します。上記例では "OpenCoverReport.xml" というファイル名を指定しています。
-filter カバレッジ結果にアセンブリとクラスを選択的に含めたり除外したりするために適用するフィルターのリストを指定します。
本例では "CounterDll" というアセンブリに含む全てのクラスを対象として指定しています。
書式については OpenCover の資料 や様々なインターネット記事を参照してください。

 

 

Step 3

"VSTest@2" を削除しているので、新たにテスト結果の発行処理を追加します。"PublishTestResults@2" というタスクを使って実現します。

このタスク無しだと、"Tests" でのテスト結果表示が無くなります。

 

["azure-pipelines.yml"]

・・・

- task: PublishTestResults@2
  displayName: "Publish Test Result"
  condition: always()
  inputs:
    testResultsFormat: 'VSTest'
    testResultsFiles: '**\TestResult.xml'

・・・

 

 

Step 4

ReportGenerator を使って OpenCover の出力レポートを整形します。

ReportGenerator は Azure DevOps 初期状態では使用できません。下記手順で Marketplace から無料で入手してインストールしておく必要があります。

 

Browse marketplace]メニューをクリックします。

[Fig: "Marketplace" ボタン位置]

Marketplace ボタン

 

下図のような画面を表示します。

Marketplace 画面

 

"ReportGenerator" で検索して、下図例では左側にある "ReportGenerator" をクリックします。

ReportGenerator

 

[Get it free]ボタンをクリックします。

"Get it free" をクリック

 

[Install]ボタンをクリックします。

Install ボタンをクリック

 

こちらの画面を表示したらインストールを完了です。[Proceed to organization]のボタンをクリックして元の画面へ戻る、またはこの画面を閉じても良いです。

インストール完了

 

これで Pipelines で "ReportGenerator" を使用できるようになりました。

"ReportGenerator" を使って OpenCover の出力レポートを整形します。"reportgenerator@5" というタスクを使って実現します。

 

["azure-pipelines.yml"]

・・・

- task: reportgenerator@5
  condition: always()
  inputs:
    reports: '**\OpenCoverReport.xml'
    targetdir: 'CoverageReport'

    reporttypes: HtmlInline_AzurePipelines;Cobertura

・・・

 

[Table: reportgenerator@5 引数について]
属性 説明
reports OpenCover が出力した解析結果(XMLファイル)を指定します。
本ページ例では "OpenCoverReport.xml" を指定します。
targetdir 結果を出力するディレクトリを指定します。
本ページ例では "CoverageReport" を指定します。
-reporttypes 出力するレポートタイプを指定します。複数指定することが可能です。
本ページ例では "HtmlInline_AzurePipelines;Cobertura" を指定します。
指定できるタイプはとてもたくさんあります。"HtmlInline_AzurePipelines_Dark" へ変更するとダーク色をテーマとするページを出力してくれます。詳細は こちら を参照。

 

ReportGenerator に関する参照ページ:

 

 

Step 5

カバレッジ結果の発行処理を追加します。"PublishCodeCoverageResults@1" というタスクを使って実現します。

 

["azure-pipelines.yml"]

・・・

- task: PublishCodeCoverageResults@1
  condition: always()
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: 'CoverageReport\Cobertura.xml'
    reportDirectory: 'CoverageReport'
  env:
    DISABLE_COVERAGE_AUTOGENERATE: 'true'

・・・

 

[Table: PublishCodeCoverageResult@1 引数について]
属性 説明
CodeCoverageTool カバレッジレポートを生成したツールを指定します。
summaryFileLocation サマリーファイルを指定します。ReportGenerator が出力する XML ファイルを指定します。ReportGenerator に対して出力するファイル名を指定することはできませんが、ReportGenerator の targetdir の場所に "フォーマット名.xml" で保存されています。
reportDIrectory レポートとして公開するディレクトリを指定します。
index.html または index.htm があればそれを表示していると思われます。

 

また下記指定を行うことで Coverage の自動生成をしないように設定します。

env:
  DISABLE_COVERAGE_AUTOGENERATE: 'true'

これを設定しないと下図のような Warnings を出力します。

Warnings

 

注意

"PublishCodeCoverageResults@2" という新しいタスクが既に公開されて使用可能ですが、これを使ってこの処理を期待通りに動作させることができませんでした。"PublishCodeCoverageResults@1" を使用するようにしてください。

解決方法を見つけたらこちらページの情報を更新したいと思います。

 

 

 

ここまでの内容を全て反映した "azure-pipelines.yml" の全体を以下に記載します。

 

["azure-pipelines.yml"]

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

# - task: VSTest@2
#   inputs:
#     testAssemblyVer2: |
#       **\$(BuildConfiguration)\testCounter1.dll
#       **\$(BuildConfiguration)\testCounter2.dll
#       **\$(BuildConfiguration)\testCounter3.dll
#       !**\obj\**
#     platform: '$(buildPlatform)'
#     configuration: '$(buildConfiguration)'
#     codeCoverageEnabled: true
#     runSettingsFile: '.\.runsettings'

- task: CmdLine@2
  displayName: "VSTest & OpenCover"
  inputs:
    script: >
      packages\OpenCover.4.7.1221\tools\OpenCover.Console.exe
      -target:"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe"
      -targetargs:".\testCounter*\bin\$(BuildConfiguration)\testCounter*.dll /logger:trx;LogFileName=TestResult.xml"
      -targetdir:"."
      -register:Path64
      -filter:"+[CounterDll]*"
      -output:"OpenCoverReport.xml"
      -returntargetcode

- task: PublishTestResults@2
  displayName: "Publish Test Result"
  condition: always()
  inputs:
    testResultsFormat: 'VSTest'
    testResultsFiles: '**\TestResult.xml'

- task: reportgenerator@5
  condition: always()
  inputs:
    reports: '**\OpenCoverReport.xml'
    targetdir: 'CoverageReport'
    reporttypes: HtmlInline_AzurePipelines_Dark;Cobertura

- task: PublishCodeCoverageResults@1
  condition: always()
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: 'CoverageReport\Cobertura.xml'
    reportDirectory: 'CoverageReport'
  env:
    DISABLE_COVERAGE_AUTOGENERATE: 'true'

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterWPF\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterWPF_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: CounterForm\bin\$(buildConfiguration)
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\CounterForm_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs - zip'
    publishLocation: Container

 

Pipelines 実行後の結果を以下に紹介します。

[Summary]画面の様子です。

Summary

 

[Tests]画面の様子です。

Tests 

 

[Code Coverage]画面の様子です。

Code coverage

 

"HtmlInline_AzurePipelines_Dark" を指定した場合の[Code Coverage]画面の様子です。

Code coverage

 

ソースコードでの詳細なカバレッジも画面上から表示できます。

Code Coverage

 

良い感じです!

 


 

8. ClickOnce

Pipelines で ClickOnce インストーラ作成を試してみました。私が期待する結果を得ることができましたので、こちらにメモを残します。

 

注意

本章に記載の内容はこれまで試したプロジェクトと環境が異なります。

下記[環境]に記載の条件で試していることにご注意ください。

 

[環境]

コンパイラ : Visual Studio 2022, Version 17.8.4
開発言語: c#  
フレームワーク: .NET 8.0  
OS : Windows11 home, 23H2

 

 

 

Step 1

Visual Studio 上で「発行」から ClickOnce 設定を行って「ClickOnceProfile.pubxml」を設定します。

本ページは Pipelines の実現方法を記載することが目的なので、発行および ClickOnce についての説明を割愛します。

発行(ClickOnce)設定

 

["ClickOnceProfile.pubxml" 例]

<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
  <PropertyGroup>
    <ApplicationRevision>1</ApplicationRevision>
    <ApplicationVersion>1.0.0.*</ApplicationVersion>
    <BootstrapperEnabled>True</BootstrapperEnabled>
    <Configuration>Release</Configuration>
    <CreateWebPageOnPublish>False</CreateWebPageOnPublish>
    <GenerateManifests>true</GenerateManifests>
    <Install>True</Install>
    <InstallFrom>Unc</InstallFrom>
    <InstallUrl>\\10.75.46.168\share$\ホゲホゲアプリ\</InstallUrl>
    <IsRevisionIncremented>True</IsRevisionIncremented>
    <IsWebBootstrapper>False</IsWebBootstrapper>
    <MapFileExtensions>True</MapFileExtensions>
    <MinimumRequiredVersion>1.0.0.0</MinimumRequiredVersion>
    <OpenBrowserOnPublish>False</OpenBrowserOnPublish>
    <Platform>Any CPU</Platform>
    <PublishDir>bin\Release\net8.0-windows10.0.22621.0\app.publish\</PublishDir>
    <PublishUrl>\\10.75.46.168\share$\ホゲホゲアプリ\</PublishUrl>
    <PublishProtocol>ClickOnce</PublishProtocol>
    <PublishReadyToRun>False</PublishReadyToRun>
    <PublishSingleFile>False</PublishSingleFile>
    <SelfContained>False</SelfContained>
    <SignatureAlgorithm>(なし)</SignatureAlgorithm>
    <SignManifests>False</SignManifests>
    <SkipPublishVerification>false</SkipPublishVerification>
    <TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
    <UpdateEnabled>True</UpdateEnabled>
    <UpdateMode>Foreground</UpdateMode>
    <UpdateRequired>True</UpdateRequired>
    <WebPageFileName>Publish.html</WebPageFileName>
    <ProductName>ホゲホゲアプリ</ProductName>
    <DisallowUrlActivation>True</DisallowUrlActivation>
    <PublisherName>ホゲ</PublisherName>
  </PropertyGroup>
  <ItemGroup>
    <BootstrapperPackage Include="Microsoft.NetCore.DesktopRuntime.8.0.x64">
      <Install>true</Install>
      <ProductName>.NET デスクトップ ランタイム 8.0.1 (x64)</ProductName>
    </BootstrapperPackage>
  </ItemGroup>
</Project>

 

Step 2

"ClickOnceProfile.pubxml" および発行に必要なファイルをリポジトリへコミットします。

 

Step 3

"azure-pipelines.yml" へ "- task: VSBuild@1" を下図のように追加します。

 

以上の内容だけだと発行先に不要なファイルを多数保存しています。DeleteFiles@1 で不要なファイルを削除します。下記ファイルおよびフォルダ以外は不要のはずです。

 

["azure-pipelines.yml"]

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main
- develop
- release/**
- hotfix/**
- feature/**

pool:
  #vmImage: 'windows-latest'
  vmImage: 'windows-2022'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  zipFilename: 'HogeHogeApp'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSBuild@1
  displayName: 'Publish'
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    msbuildArgs: '-t:publish -p:PublishProfile=Properties\PublishProfiles\ClickOnceProfile.pubxml -p:PublishDir="bin/Publish"'

- task: DeleteFiles@1
  displayName: 'Remove unneeded files'
  inputs:
    contents: |
      bin/Publish/HogeHogeApp.exe
      bin/Publish/Launcher.exe
      bin/Publish/*.dll
      bin/Publish/*.pdb
      bin/Publish/*.json

- task: ArchiveFiles@2
  inputs:
    rootFolderOrFile: bin\
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStrageDirectory)\$(zipFilename)_$(Build.BuildId).zip'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStrageDirectory)\$(zipFilename)_$(Build.BuildId).zip'
    artifactName: 'MyBuildOutputs'
    publishLocation: Container

 

Step 4

最後にあなたが希望する場所へデプロイします。

上記 "azure-pipelines.yml" 例では zip 圧縮してアーティファクトとして発行しています。

 

Step 5

デプロイした結果を取得して確認します。

下図は取得した zip ファイルを解凍した "bin/Publish/" フォルダ内の様子です。ClickOnce インストーラ一式が保存されていることを確認できます。

ClickOnce インストーラ

 

以上です。

 

 


 

ライセンス

本ページの情報は、特記無い限り下記 MIT ライセンスで提供されます。

The MIT License (MIT)

  Copyright 2024 Kinoshita Hidetoshi

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

 

 

参考

 


 

変更履歴

2024-02-11 - 6. カバレッジ計測」を更新
2024-02-06 - 8. ClickOnce」を追加
2024-01-25 - 新規作成