Skip to content

Commit

Permalink
[QT-696] Add enos scenario outline (#128)
Browse files Browse the repository at this point in the history
* [QT-696] Add `enos scenario outline`

Add metadata support to Enos scenarios to allow us to quickly read an
outline of a scenario and determine what quality characteristics are
verified.

We introduce new DSL schema:
  - `quality`: A new top-level object that describes a quality
    characteristic.
  - `quality.description`: An required string attribute in the `quality`
    block that describes the quality characteristic in detail.
  - `scenario.description`: An optional string attribute that describes
    the scenario.
  - `scenario.step.description`: An optional string attribute that
    describes the scenario step.
  - `scenario.step.verifies`: An optional `quality` attribute. Can be
    either a singular `quality` or a list of `quality`'s.

Using these new blocks and attributes a scenario author can now more
fully describe the purpose of the scenario, each step, and which quality
characteristics are verified by it.

To see the generated outline you can use `enos scenario outline
<optional-filter>` to generate a scenario outline.

The `scenario outline` sub-command supports a new output formatter:
`html`. Use the HTML formatter and your browser for a cleaner overview
if so desired, e.g.:

```
enos scenario outline --format html > outline.html
open outline.html
```

* Fix deprecations in `.golangci.yml`
* Fix warnings output by `golangci-lint`
* Silence `buf` lint error that is irrelevant to us
* Support decoding `quality` blocks
* Support decoding `scenario.description`
* Support decoding `scenario.step.description`
* Support decoding `scenario.verifies`
* Add `OutlineScenario()` RPC to the enos service.
* Add acceptance test for `enos scenario outline`
* Restructure matrix decoder into its own type. This allows us to keep a
  copy of the original matrix around to use in the outline
* Add new decoder target for scenario outlines
* Add `ShowOutline()` to `basic` UI
* Add `ShowOutline()` to `machine` UI
* Create a shim HTML UI
* Add `ShowOutline()` to `html` UI
* Bump version to 0.0.28
* Add missing documenation for `globals` DSL to `README.md`
* Add missing documenation for `sample` DSL to `README.md`
* Add documenation for `quality` DSL to `README.md`
* Add missing documenation for `enos scenario sample` command to
  `README.md`
* Add documenation for `enos scenario outline` command to `README.md`

Signed-off-by: Ryan Cragun <[email protected]>
  • Loading branch information
ryancragun authored Apr 9, 2024
1 parent e10f3ab commit d654099
Show file tree
Hide file tree
Showing 45 changed files with 4,605 additions and 2,106 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ env:

jobs:
product-metadata:
runs-on: linux
runs-on: ubuntu-latest
outputs:
filepath: ${{ steps.generate-metadata-file.outputs.filepath }}
product-version: ${{ steps.product-metadata.outputs.product-version }}
Expand All @@ -34,7 +34,7 @@ jobs:
path: ${{ steps.generate-metadata-file.outputs.filepath }}

profile-binary:
runs-on: linux
runs-on: ubuntu-latest
env:
GOPRIVATE: 'github.com/hashicorp/*'
TOKEN: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:
needs:
- product-metadata
- profile-binary
runs-on: linux
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, darwin]
Expand Down Expand Up @@ -147,7 +147,7 @@ jobs:
needs:
- product-metadata
- build
runs-on: linux
runs-on: ubuntu-latest
strategy:
matrix:
arch: ["arm64", "amd64"]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
# If the PR does not have one of the four designated `changelog/` labels applied, this job will fail.
check-for-changelog-label:
runs-on: linux
runs-on: ubuntu-latest
if: (!contains(github.event.pull_request.labels.*.name, 'changelog/none')) && (!contains(github.event.pull_request.labels.*.name, 'changelog/bug')) && (!contains(github.event.pull_request.labels.*.name, 'changelog/feat')) && (!contains(github.event.pull_request.labels.*.name, 'changelog/other'))
steps:
- name: Changelog label not found
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/create_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ on:
jobs:
create-release:
name: Create Release
runs-on: linux
runs-on: ubuntu-latest
env:
GOPRIVATE: 'github.com/hashicorp/*'
GITHUB_TOKEN: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ env:
jobs:
build-artifact:
name: Build Linux Artifact
runs-on: linux
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update_homebrew_formula.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ on:
jobs:
update-formula:
name: "Update Homebrew formula definition"
runs-on: linux
runs-on: ubuntu-latest
env:
# Note: `gh` CLI automatically looks for and uses `env.GH_TOKEN` for authentication.
# This token must have read:org scope in order to authenticate on a different repo.
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ on:
jobs:
static-analysis:
name: "Format"
runs-on: linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
Expand Down Expand Up @@ -48,7 +48,7 @@ jobs:

test:
name: Go tests
runs-on: linux
runs-on: ubuntu-latest
env:
CI: true
steps:
Expand Down
3 changes: 2 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ run:
output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
# default is "colored-line-number"
format: colored-line-number
formats:
- format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ lint-fix-golang:

.PHONY: lint-proto
lint-proto:
pushd proto && buf lint --error-format=$(BUF_LINT_OUT_FORMAT)
buf lint --error-format=$(BUF_LINT_OUT_FORMAT)

.PHONY: fmt
fmt: fmt-golang fmt-proto fmt-terraform fmt-enos
Expand Down
271 changes: 235 additions & 36 deletions README.md

Large diffs are not rendered by default.

166 changes: 166 additions & 0 deletions acceptance/scenario_outline_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package acceptance

import (
"context"
"fmt"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"

"github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb"
)

func TestAcc_Cmd_Scenario_Outline(t *testing.T) {
t.Parallel()

for _, test := range []struct {
dir string
out *pb.OutlineScenariosResponse
fail bool
}{
{
dir: "scenarios/scenario_outline",
out: &pb.OutlineScenariosResponse{
Outlines: []*pb.Scenario_Outline{
{
Scenario: &pb.Ref_Scenario{
Id: &pb.Scenario_ID{
Name: "multiple_verifies",
Description: "This is a multiline description\nof the upgrade scenario.\n",
},
},
Matrix: &pb.Matrix{
Vectors: []*pb.Matrix_Vector{
{Elements: []*pb.Matrix_Element{
{Key: "arch", Value: "amd64"},
{Key: "arch", Value: "arm64"},
}},
{Elements: []*pb.Matrix_Element{
{Key: "distro", Value: "ubuntu"},
{Key: "distro", Value: "rhel"},
}},
},
},
Steps: []*pb.Scenario_Outline_Step{
{
Name: "test",
Description: "This is an indented\nmultiline step description.\n",
Verifies: []*pb.Quality{
{
Name: "inline",
Description: "an inline quality that isn't reused",
},
{
Name: "the_data_is_durable",
Description: "The data is durable\nafter an upgrade.\n",
},
{
Name: "the_tests_pass",
Description: "The tests all pass!",
},
},
},
},
Verifies: []*pb.Quality{
{
Name: "inline",
Description: "an inline quality that isn't reused",
},
{
Name: "the_data_is_durable",
Description: "The data is durable\nafter an upgrade.\n",
},
{
Name: "the_tests_pass",
Description: "The tests all pass!",
},
},
},
{
Scenario: &pb.Ref_Scenario{
Id: &pb.Scenario_ID{
Name: "singular_verifies",
Description: "This is a multiline description\nof the upgrade scenario.\n",
},
},
Matrix: &pb.Matrix{
Vectors: []*pb.Matrix_Vector{
{Elements: []*pb.Matrix_Element{
{Key: "arch", Value: "amd64"},
{Key: "arch", Value: "arm64"},
}},
{Elements: []*pb.Matrix_Element{
{Key: "distro", Value: "ubuntu"},
{Key: "distro", Value: "rhel"},
}},
},
},
Steps: []*pb.Scenario_Outline_Step{
{
Name: "test",
Description: "This is an indented\nmultiline step description.\n",
Verifies: []*pb.Quality{
{
Name: "the_tests_pass",
Description: "The tests all pass!",
},
},
},
},
Verifies: []*pb.Quality{
{
Name: "the_tests_pass",
Description: "The tests all pass!",
},
},
},
},
},
},
} {
t.Run(test.dir, func(t *testing.T) {
t.Parallel()
enos := newAcceptanceRunner(t)

path, err := filepath.Abs(filepath.Join("./", test.dir))
require.NoError(t, err)
cmd := fmt.Sprintf("scenario outline --chdir %s --format json", path)
out, err := enos.run(context.Background(), cmd)
if test.fail {
require.Error(t, err)

return
}

require.NoError(t, err)
got := &pb.OutlineScenariosResponse{}
require.NoError(t, protojson.Unmarshal(out, got))
require.Len(t, got.GetOutlines(), len(test.out.GetOutlines()))
for i := range test.out.GetOutlines() {
require.EqualValues(t, test.out.GetOutlines()[i].GetScenario().GetId().GetName(), got.GetOutlines()[i].GetScenario().GetId().GetName())
require.EqualValues(t, test.out.GetOutlines()[i].GetScenario().GetId().GetDescription(), got.GetOutlines()[i].GetScenario().GetId().GetDescription())
require.EqualValues(t, test.out.GetOutlines()[i].GetScenario().GetId().GetFilter(), got.GetOutlines()[i].GetScenario().GetId().GetFilter())
require.EqualValues(t, test.out.GetOutlines()[i].GetScenario().GetId().GetUid(), got.GetOutlines()[i].GetScenario().GetId().GetUid())
require.EqualValues(t, test.out.GetOutlines()[i].GetMatrix().String(), got.GetOutlines()[i].GetMatrix().String())
require.Len(t, got.GetOutlines()[i].GetVerifies(), len(test.out.GetOutlines()[i].GetVerifies()))
for q := range test.out.GetOutlines()[i].GetVerifies() {
require.EqualValues(t, test.out.GetOutlines()[i].GetVerifies()[q].GetName(), got.GetOutlines()[i].GetVerifies()[q].GetName())
require.EqualValues(t, test.out.GetOutlines()[i].GetVerifies()[q].GetDescription(), got.GetOutlines()[i].GetVerifies()[q].GetDescription())
}
for s := range test.out.GetOutlines()[i].GetSteps() {
require.EqualValues(t, test.out.GetOutlines()[i].GetSteps()[s].GetName(), got.GetOutlines()[i].GetSteps()[s].GetName())
require.EqualValues(t, test.out.GetOutlines()[i].GetSteps()[s].GetDescription(), got.GetOutlines()[i].GetSteps()[s].GetDescription())
for q := range test.out.GetOutlines()[i].GetSteps()[s].GetVerifies() {
require.EqualValues(t, test.out.GetOutlines()[i].GetSteps()[s].GetVerifies()[q].GetName(), got.GetOutlines()[i].GetSteps()[s].GetVerifies()[q].GetName())
require.EqualValues(t, test.out.GetOutlines()[i].GetSteps()[s].GetVerifies()[q].GetDescription(), got.GetOutlines()[i].GetSteps()[s].GetVerifies()[q].GetDescription())
}
}
}
})
}
}
80 changes: 80 additions & 0 deletions acceptance/scenarios/scenario_outline/enos.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

module "test" {
source = "../scenario_generate_pass_0/modules/foo"
}

quality "the_tests_pass" {
description = "The tests all pass!"
}

quality "the_data_is_durable" {
description = <<-EOF
The data is durable
after an upgrade.
EOF
}

scenario "singular_verifies" {
description = <<EOF
This is a multiline description
of the upgrade scenario.
EOF

matrix {
arch = ["amd64", "arm64"]
distro = ["ubuntu", "rhel"]
}

step "test" {
description = <<-EOF
This is an indented
multiline step description.
EOF

verifies = quality.the_tests_pass

module = module.test

variables {
input = matrix.arch
anotherinput = matrix.distro
}
}
}

scenario "multiple_verifies" {
description = <<EOF
This is a multiline description
of the upgrade scenario.
EOF

matrix {
arch = ["amd64", "arm64"]
distro = ["ubuntu", "rhel"]
}

step "test" {
description = <<-EOF
This is an indented
multiline step description.
EOF

verifies = [
quality.the_tests_pass,
{
name : "inline",
description : "an inline quality that isn't reused",
},
quality.the_data_is_durable,
]

module = module.test

variables {
input = matrix.arch
anotherinput = matrix.distro
}
}
}
3 changes: 3 additions & 0 deletions buf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ breaking:
lint:
use:
- DEFAULT
ignore_only:
PACKAGE_DIRECTORY_MATCH:
- proto/hashicorp/enos/v1/enos.proto
4 changes: 4 additions & 0 deletions internal/command/enos/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ func setupCLIUI() error {
uiCfg.Format = pb.UI_Settings_FORMAT_JSON
}

if rootState.format == "html" {
uiCfg.Format = pb.UI_Settings_FORMAT_HTML
}

if term.IsTerminal(int(os.Stdout.Fd())) {
uiCfg.IsTty = true
uiCfg.UseColor = true
Expand Down
1 change: 1 addition & 0 deletions internal/command/enos/cmd/scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func newScenarioCmd() *cobra.Command {
scenarioCmd.AddCommand(newScenarioOutputCmd())
scenarioCmd.AddCommand(newScenarioValidateConfigCmd())
scenarioCmd.AddCommand(newScenarioSampleCmd())
scenarioCmd.AddCommand(newScenarioOutlineCmd())

return scenarioCmd
}
Expand Down
Loading

0 comments on commit d654099

Please sign in to comment.