Help us understand the problem. What is going on with this article?

GoogleのContainer Structure TestsをMacOSで実行してみる

More than 1 year has passed since last update.

先日Googleから Container Structure Tests: Unit Tests for Docker Images | Google Open Source Blog というお話が出ていました。

やってみよう。

テストできるのは次のパターン。

  • コマンド実行結果
  • ファイルの場所とパーミッション
  • ファイルの中身
  • コンテナイメージのメタデータ(EXPOSEとかWORKDIRなど)
  • 含まれるOSSのライセンス (GCP上で動かしてもよいかのチェックなのかな?とりあえずdebianのみ対応)

内容からして、ファイルの記述が正しければ動作も正しいだろう、という観点で使うもののようですね。 Structureっつってんだからそういうもんでしょう。

MacOSでのセットアップ

配布しているバイナリはLinuxでのみ動くよ、とのことです。コンテナを配布しているのでそっちを使ってみましょう。

コンテナはこちらに。gcr.io/gcp-runtimes/container-structure-test

$ docker pull gcr.io/gcp-runtimes/container-structure-test:v0.1.3
v0.1.3: Pulling from gcp-runtimes/container-structure-test
cd2f5b7886e9: Pull complete 
9be56f236f9a: Pull complete 
Digest: sha256:15ae6f3996ebc05b657a042bd5dd8f4d4d66b015effea5fd024c70b8a1f9c42a
Status: Downloaded newer image for gcr.io/gcp-runtimes/container-structure-test:v0.1.3

ヘルプを出してみます。

$ docker run gcr.io/gcp-runtimes/container-structure-test:v0.1.3 -h
Usage of /go_default_test:
  -driver string
        driver to use when running tests (default "docker")
  -image string
        path to test image
  -pull
        force a pull of the image before running tests
  -save
        preserve created containers after test run
  -test.bench regexp
        run only benchmarks matching regexp
  -test.benchmem
        print memory allocations for benchmarks
  -test.benchtime d
        run each benchmark for duration d (default 1s)
  -test.blockprofile file
        write a goroutine blocking profile to file
  -test.blockprofilerate rate
        set blocking profile rate (see runtime.SetBlockProfileRate) (default 1)
  -test.count n
        run tests and benchmarks n times (default 1)
  -test.coverprofile file
        write a coverage profile to file
  -test.cpu list
        comma-separated list of cpu counts to run each test with
  -test.cpuprofile file
        write a cpu profile to file
  -test.memprofile file
        write a memory profile to file
  -test.memprofilerate rate
        set memory profiling rate (see runtime.MemProfileRate)
  -test.mutexprofile string
        write a mutex contention profile to the named file after execution
  -test.mutexprofilefraction int
        if >= 0, calls runtime.SetMutexProfileFraction() (default 1)
  -test.outputdir dir
        write profiles to dir
  -test.parallel n
        run at most n tests in parallel (default 3)
  -test.run regexp
        run only tests and examples matching regexp
  -test.short
        run smaller test suite to save time
  -test.timeout d
        fail test binary execution after duration d (0 means unlimited)
  -test.trace file
        write an execution trace to file
  -test.v
        verbose: print additional output

よしよし。

サンプルのテストを実行する

リポジトリにテスト用のファイルがあるので、それを使ってみましょう。container-structure-test/tests/debian_test.yaml

メタデータ以外のテストが網羅されていますね。

まずMacOSでdockerドライバ(default)をつかってみます。docker.sockをマウントすればOKでしょうきっと。コンフィグファイルもマウントしましょう、ルート直下でOK。

このとき-pullをつけておくとイメージタグが必須になり(-pullでなければローカルからlatestを探索)、テストの前にpullしてくれます。

$ docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v `pwd`/debian_test.yaml:/debian_test.yaml \
  gcr.io/gcp-runtimes/container-structure-test:v0.1.3 \
  -pull \
  -test.v \
  --image gcr.io/google-appengine/debian8:latest \
  debian_test.yaml

latest: Pulling from google-appengine/debian8
Digest: sha256:412ef4d53215ff4a95d275ad48fe5196cb51f4f96b99c05058054b3bdf9443c1
Status: Image is up to date for gcr.io/google-appengine/debian8:latest
Using driver docker
=== RUN   TestAll
=== RUN   TestAll/Command_Test:_apt-get
2018/01/11 06:40:59 Running tests for file debian_test.yaml
=== RUN   TestAll/Command_Test:_apt-config
=== RUN   TestAll/Command_Test:_path
=== RUN   TestAll/File_Existence_Test:_Root
=== RUN   TestAll/File_Existence_Test:_Netbase
=== RUN   TestAll/File_Existence_Test:_Machine_ID
=== RUN   TestAll/File_Content_Test:_Debian_Sources
=== RUN   TestAll/File_Content_Test:_Retry_Policy
=== RUN   TestAll/Metadata_Test
=== RUN   TestAll/License_Test_#0
--- PASS: TestAll (36.99s)
    --- PASS: TestAll/Command_Test:_apt-get (1.25s)
        docker_driver.go:74: stdout: apt 1.0.9.8.4 for amd64 compiled on Dec 11 2016 09:48:19
            Usage: apt-get [options] command
                   apt-get [options] install|remove pkg1 [pkg2 ...]
                   apt-get [options] source pkg1 [pkg2 ...]

            apt-get is a simple command line interface for downloading and
            installing packages. The most frequently used commands are update
            and install.

            Commands:
               update - Retrieve new lists of packages
               upgrade - Perform an upgrade
               install - Install new packages (pkg is libc6 not libc6.deb)
               remove - Remove packages
               autoremove - Remove automatically all unused packages
               purge - Remove packages and config files
               source - Download source archives
               build-dep - Configure build-dependencies for source packages
               dist-upgrade - Distribution upgrade, see apt-get(8)
               dselect-upgrade - Follow dselect selections
               clean - Erase downloaded archive files
               autoclean - Erase old downloaded archive files
               check - Verify that there are no broken dependencies
               changelog - Download and display the changelog for the given package
               download - Download the binary package into the current directory

            Options:
              -h  This help text.
              -q  Loggable output - no progress indicator
              -qq No output except for errors
              -d  Download only - do NOT install or unpack archives
              -s  No-act. Perform ordering simulation
              -y  Assume Yes to all queries and do not prompt
              -f  Attempt to correct a system with broken dependencies in place
              -m  Attempt to continue if archives are unlocatable
              -u  Show a list of upgraded packages as well
              -b  Build the source package after fetching it
              -V  Show verbose version numbers
              -c=? Read this configuration file
              -o=? Set an arbitrary configuration option, eg -o dir::cache=/tmp
            See the apt-get(8), sources.list(5) and apt.conf(5) manual
            pages for more information and options.
                                   This APT has Super Cow Powers.
    --- PASS: TestAll/Command_Test:_apt-config (1.98s)
        docker_driver.go:74: stdout: APT "";
            APT::Architecture "amd64";
            APT::Build-Essential "";
            APT::Build-Essential:: "build-essential";
            APT::Install-Recommends "1";
            APT::Install-Suggests "0";
            APT::NeverAutoRemove "";
            APT::NeverAutoRemove:: "^firmware-linux.*";
            APT::NeverAutoRemove:: "^linux-firmware$";
            APT::VersionedKernelPackages "";
            APT::VersionedKernelPackages:: "linux-image";
            APT::VersionedKernelPackages:: "linux-headers";
            APT::VersionedKernelPackages:: "linux-image-extra";
            APT::VersionedKernelPackages:: "linux-signed-image";
            APT::VersionedKernelPackages:: "kfreebsd-image";
            APT::VersionedKernelPackages:: "kfreebsd-headers";
            APT::VersionedKernelPackages:: "gnumach-image";
            APT::VersionedKernelPackages:: ".*-modules";
            APT::VersionedKernelPackages:: ".*-kernel";
            APT::VersionedKernelPackages:: "linux-backports-modules-.*";
            APT::VersionedKernelPackages:: "linux-tools";
            APT::Never-MarkAuto-Sections "";
            APT::Never-MarkAuto-Sections:: "metapackages";
            APT::Never-MarkAuto-Sections:: "restricted/metapackages";
            APT::Never-MarkAuto-Sections:: "universe/metapackages";
            APT::Never-MarkAuto-Sections:: "multiverse/metapackages";
            APT::Never-MarkAuto-Sections:: "oldlibs";
            APT::Never-MarkAuto-Sections:: "restricted/oldlibs";
            APT::Never-MarkAuto-Sections:: "universe/oldlibs";
            APT::Never-MarkAuto-Sections:: "multiverse/oldlibs";
            APT::AutoRemove "";
            APT::AutoRemove::SuggestsImportant "false";
            APT::Update "";
            APT::Update::Post-Invoke "";
            APT::Update::Post-Invoke:: "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true";
            APT::Architectures "";
            APT::Architectures:: "amd64";
            APT::Compressor "";
            APT::Compressor::. "";
            APT::Compressor::.::Name ".";
            APT::Compressor::.::Extension "";
            APT::Compressor::.::Binary "";
            APT::Compressor::.::Cost "1";
            APT::Compressor::gzip "";
            APT::Compressor::gzip::Name "gzip";
            APT::Compressor::gzip::Extension ".gz";
            APT::Compressor::gzip::Binary "gzip";
            APT::Compressor::gzip::Cost "2";
            APT::Compressor::gzip::CompressArg "";
            APT::Compressor::gzip::CompressArg:: "-9n";
            APT::Compressor::gzip::UncompressArg "";
            APT::Compressor::gzip::UncompressArg:: "-d";
            APT::Compressor::bzip2 "";
            APT::Compressor::bzip2::Name "bzip2";
            APT::Compressor::bzip2::Extension ".bz2";
            APT::Compressor::bzip2::Binary "false";
            APT::Compressor::bzip2::Cost "3";
            APT::Compressor::xz "";
            APT::Compressor::xz::Name "xz";
            APT::Compressor::xz::Extension ".xz";
            APT::Compressor::xz::Binary "false";
            APT::Compressor::xz::Cost "4";
            APT::Compressor::lzma "";
            APT::Compressor::lzma::Name "lzma";
            APT::Compressor::lzma::Extension ".lzma";
            APT::Compressor::lzma::Binary "false";
            APT::Compressor::lzma::Cost "5";
            APT::Compressor::lzma::CompressArg "";
            APT::Compressor::lzma::CompressArg:: "--suffix=";
            APT::Compressor::lzma::CompressArg:: "-9";
            APT::Compressor::lzma::UncompressArg "";
            APT::Compressor::lzma::UncompressArg:: "--suffix=";
            APT::Compressor::lzma::UncompressArg:: "-d";
            Dir "/";
            Dir::State "var/lib/apt/";
            Dir::State::lists "lists/";
            Dir::State::cdroms "cdroms.list";
            Dir::State::mirrors "mirrors/";
            Dir::State::extended_states "extended_states";
            Dir::State::status "/var/lib/dpkg/status";
            Dir::Cache "var/cache/apt/";
            Dir::Cache::archives "archives/";
            Dir::Cache::srcpkgcache "";
            Dir::Cache::pkgcache "";
            Dir::Etc "etc/apt/";
            Dir::Etc::sourcelist "sources.list";
            Dir::Etc::sourceparts "sources.list.d";
            Dir::Etc::vendorlist "vendors.list";
            Dir::Etc::vendorparts "vendors.list.d";
            Dir::Etc::main "apt.conf";
            Dir::Etc::netrc "auth.conf";
            Dir::Etc::parts "apt.conf.d";
            Dir::Etc::preferences "preferences";
            Dir::Etc::preferencesparts "preferences.d";
            Dir::Etc::trusted "trusted.gpg";
            Dir::Etc::trustedparts "trusted.gpg.d";
            Dir::Bin "";
            Dir::Bin::methods "/usr/lib/apt/methods";
            Dir::Bin::solvers "";
            Dir::Bin::solvers:: "/usr/lib/apt/solvers";
            Dir::Bin::dpkg "/usr/bin/dpkg";
            Dir::Bin::bzip2 "/bin/bzip2";
            Dir::Bin::xz "/usr/bin/xz";
            Dir::Bin::lzma "/usr/bin/lzma";
            Dir::Media "";
            Dir::Media::MountPath "/media/apt";
            Dir::Log "var/log/apt";
            Dir::Log::Terminal "term.log";
            Dir::Log::History "history.log";
            Dir::Ignore-Files-Silently "";
            Dir::Ignore-Files-Silently:: "~$";
            Dir::Ignore-Files-Silently:: "\.disabled$";
            Dir::Ignore-Files-Silently:: "\.bak$";
            Dir::Ignore-Files-Silently:: "\.dpkg-[a-z]+$";
            Dir::Ignore-Files-Silently:: "\.save$";
            Dir::Ignore-Files-Silently:: "\.orig$";
            Dir::Ignore-Files-Silently:: "\.distUpgrade$";
            Acquire "";
            Acquire::cdrom "";
            Acquire::cdrom::mount "/media/cdrom/";
            Acquire::Retries "3";
            Acquire::GzipIndexes "true";
            Acquire::CompressionTypes "";
            Acquire::CompressionTypes::Order "";
            Acquire::CompressionTypes::Order:: "gz";
            Acquire::Languages "";
            Acquire::Languages:: "none";
            DPkg "";
            DPkg::Pre-Install-Pkgs "";
            DPkg::Pre-Install-Pkgs:: "/usr/sbin/dpkg-preconfigure --apt || true";
            DPkg::Post-Invoke "";
            DPkg::Post-Invoke:: "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true";
            CommandLine "";
            CommandLine::AsString "apt-config dump";
    --- PASS: TestAll/Command_Test:_path (1.54s)
        docker_driver.go:74: stdout: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    --- PASS: TestAll/File_Existence_Test:_Root (0.67s)
    --- PASS: TestAll/File_Existence_Test:_Netbase (0.16s)
    --- PASS: TestAll/File_Existence_Test:_Machine_ID (0.16s)
    --- PASS: TestAll/File_Content_Test:_Debian_Sources (0.14s)
    --- PASS: TestAll/File_Content_Test:_Retry_Policy (0.18s)
    --- PASS: TestAll/Metadata_Test (0.00s)
    --- PASS: TestAll/License_Test_#0 (30.90s)
        licenses.go:70: acl
        licenses.go:70: adduser
        licenses.go:70: apt
        licenses.go:70: base-files
        licenses.go:70: base-passwd
        licenses.go:70: bash
        licenses.go:70: bsdutils
        licenses.go:70: ca-certificates
        licenses.go:70: coreutils
        licenses.go:70: dash
        licenses.go:70: debconf
        licenses.go:70: debian-archive-keyring
        licenses.go:70: debianutils
        licenses.go:70: diffutils
        licenses.go:70: dmsetup
        licenses.go:70: dpkg
        licenses.go:70: e2fslibs
        licenses.go:70: e2fsprogs
        licenses.go:70: findutils
        licenses.go:70: gcc-4.8-base
        licenses.go:70: gcc-4.9-base
        licenses.go:70: gnupg
        licenses.go:70: gpgv
        licenses.go:70: grep
        licenses.go:70: gzip
        licenses.go:70: hostname
        licenses.go:70: init
        licenses.go:70: initscripts
        licenses.go:70: insserv
        licenses.go:70: libacl1
        licenses.go:70: libapt-pkg4.12
        licenses.go:70: libattr1
        licenses.go:70: libaudit-common
        licenses.go:70: libaudit1
        licenses.go:70: libblkid1
        licenses.go:70: libbz2-1.0
        licenses.go:70: libc-bin
        licenses.go:70: libc6
        licenses.go:70: libcap2
        licenses.go:70: libcap2-bin
        licenses.go:70: libcomerr2
        licenses.go:70: libcryptsetup4
        licenses.go:70: libdb5.3
        licenses.go:70: libdebconfclient0
        licenses.go:70: libdevmapper1.02.1
        licenses.go:70: libgcrypt20
        licenses.go:70: libgpg-error0
        licenses.go:70: libkmod2
        licenses.go:70: liblocale-gettext-perl
        licenses.go:70: liblzma5
        licenses.go:70: libmount1
        licenses.go:70: libpam-modules
        licenses.go:70: libpam-modules-bin
        licenses.go:70: libpam-runtime
        licenses.go:70: libpam0g
        licenses.go:70: libpcre3
        licenses.go:70: libprocps3
        licenses.go:70: libreadline6
        licenses.go:70: libselinux1
        licenses.go:70: libsemanage-common
        licenses.go:70: libsemanage1
        licenses.go:70: libsepol1
        licenses.go:70: libslang2
        licenses.go:70: libsmartcols1
        licenses.go:70: libss2
        licenses.go:70: libssl1.0.0
        licenses.go:70: libsystemd0
        licenses.go:70: libtext-charwidth-perl
        licenses.go:70: libtext-iconv-perl
        licenses.go:70: libtext-wrapi18n-perl
        licenses.go:70: libtinfo5
        licenses.go:70: libudev1
        licenses.go:70: libusb-0.1-4
        licenses.go:70: libustr-1.0-1
        licenses.go:70: libuuid1
        licenses.go:70: login
        licenses.go:70: lsb-base
        licenses.go:70: mawk
        licenses.go:70: mount
        licenses.go:70: multiarch-support
        licenses.go:70: ncurses-base
        licenses.go:70: ncurses-bin
        licenses.go:70: netbase
        licenses.go:70: openssl
        licenses.go:70: passwd
        licenses.go:70: perl
        licenses.go:70: procps
        licenses.go:70: readline-common
        licenses.go:70: sed
        licenses.go:70: sensible-utils
        licenses.go:70: startpar
        licenses.go:70: systemd
        licenses.go:70: systemd-sysv
        licenses.go:70: sysv-rc
        licenses.go:70: sysvinit-utils
        licenses.go:70: tar
        licenses.go:70: tzdata
        licenses.go:70: util-linux
        licenses.go:70: zlib1g
    structure_test.go:47: Total tests run: 10
PASS

最後にPASSでおわればOKです。-test.vをつけない場合はPASSだけでます。FAIL時はexit_codeもちゃんと0以外で終わるので安心。

metadataTestもやってみよう

サンプルにはmetadataTestが入ってませんでした。imageをinspectして、定義をちょろっと書いてみます。

metatest.yaml
schemaVersion: '2.0.0'
metadataTest:
  env:
    - key: "PORT"
      value: "8080"
  entrypoint: []
  cmd: ["/bin/sh", "-c", "/bin/bash"]

ファイル指定を変更し、実行します。

$ docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v `pwd`/metatest.yaml:/metatest.yaml \
  gcr.io/gcp-runtimes/container-structure-test:v0.1.3 \
  -pull \
  -test.v \
  --image gcr.io/google-appengine/debian8:latest \
  metatest.yaml

latest: Pulling from google-appengine/debian8
Digest: sha256:412ef4d53215ff4a95d275ad48fe5196cb51f4f96b99c05058054b3bdf9443c1
Status: Image is up to date for gcr.io/google-appengine/debian8:latest
Using driver docker
=== RUN   TestAll
=== RUN   TestAll/Metadata_Test
2018/01/11 06:54:58 Running tests for file metatest.yaml
--- PASS: TestAll (0.01s)
    --- PASS: TestAll/Metadata_Test (0.00s)
    structure_test.go:47: Total tests run: 1
PASS

無事にテストできたようですね。

driverにtarを指定、 dockerdなしでのチェック

さて、冒頭にもリンクをおいたGoogleの記事では、dockerdがいなくてもテストできるぞ!と書いてあったので-driver tarを指定、dockerのソケットを外してみます。

$ docker run --rm \
  -v `pwd`/debian_test.yaml:/debian_test.yaml \
  gcr.io/gcp-runtimes/container-structure-test:v0.1.3 \
  -driver tar \
  -test.v \
  --image gcr.io/google-appengine/debian8:latest \
  debian_test.yaml


Using driver tar
=== RUN   TestAll
2018/01/11 06:44:51 Running tests for file debian_test.yaml
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry

-- 省略
--- FAIL: TestAll (19.89s)
    --- FAIL: TestAll/Command_Test:_apt-get (10.03s)
        tar_driver.go:66: Tar driver is unable to process commands, please use a different driver
    --- PASS: TestAll/Metadata_Test (9.86s)
    structure_test.go:47: Total tests run: 1

commandTestsは流石にダメっぽいです。commandTestsのセクションをコメントアウトして、もう一度実行。

Using driver tar
=== RUN   TestAll
2018/01/11 07:18:55 Running tests for file debian_test.yaml
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
=== RUN   TestAll/File_Existence_Test:_Root
time="2018-01-11T07:19:04Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:04Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest743647843 from system"
=== RUN   TestAll/File_Existence_Test:_Netbase
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
time="2018-01-11T07:19:14Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:14Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest188699238 from system"
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
=== RUN   TestAll/File_Existence_Test:_Machine_ID
time="2018-01-11T07:19:24Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:24Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest359712397 from system"
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
=== RUN   TestAll/File_Content_Test:_Debian_Sources
time="2018-01-11T07:19:34Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:34Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest785687176 from system"
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
=== RUN   TestAll/File_Content_Test:_Retry_Policy
time="2018-01-11T07:19:44Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:44Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest267774023 from system"
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry

... 省略

        licenses.go:70: startpar
        licenses.go:70: systemd
        licenses.go:70: systemd-sysv
        licenses.go:70: sysv-rc
        licenses.go:70: sysvinit-utils
        licenses.go:70: tar
        licenses.go:70: tzdata
        licenses.go:70: util-linux
        licenses.go:70: zlib1g
    structure_test.go:47: Total tests run: 7
PASS

PASSしました。テストごとにファイルを展開削除しているようで、これはとても時間がかかる。

metadataTestはどうなるんだろう。

$ docker run --rm \
  -v `pwd`/metatest.yaml:/metatest.yaml \
  gcr.io/gcp-runtimes/container-structure-test:v0.1.3 \
  -driver tar \
  -test.v \
  --image gcr.io/google-appengine/debian8:latest \
  metatest.yaml


Using driver tar
=== RUN   TestAll
=== RUN   TestAll/Metadata_Test
2018/01/11 06:56:09 Running tests for file metatest.yaml
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
time="2018-01-11T06:56:19Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T06:56:19Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest068032231 from system"
--- PASS: TestAll (10.13s)
    --- PASS: TestAll/Metadata_Test (10.13s)
    structure_test.go:47: Total tests run: 1
PASS

お、ちゃんとできるね。commandTestsは邪道感があるので、これでよいとするのが理想ではありますね。普通にCIを動かすLinux環境ならファイルの展開削除も早かろうし。

おわりに

コンテナ特化で、いつものコマンド+簡潔な結果期待の形式で読みやすいYamlでかけてチェックできるので良いとおもいます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした