Skip to content

Commit

Permalink
approvals: Add check-approvals and approvaltest (#6)
Browse files Browse the repository at this point in the history
Adds two new tools, `check-approvals` and an `approvaltest` package. The
latter allows clients to store a receive events and store them in json
`.received.json` suffixed files, while the former checks if the received
files match the `.approved.json` suffixed files, and prompts whether the
changes want to be accepted or not.

---------

Signed-off-by: Marc Lopez Rubio <marc5.12@outlook.com>
  • Loading branch information
marclop authored Jul 10, 2023
1 parent 4143a8e commit 2700b8f
Show file tree
Hide file tree
Showing 5 changed files with 369 additions and 0 deletions.
106 changes: 106 additions & 0 deletions cmd/check-approvals/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package main

import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/fatih/color"
"github.com/google/go-cmp/cmp"

"github.com/elastic/apm-tools/pkg/approvaltest"
)

func main() {
os.Exit(approval())
}

func approval() int {
cwd, _ := os.Getwd()
receivedFiles := findFiles(cwd, approvaltest.ReceivedSuffix)

for _, rf := range receivedFiles {
af := strings.TrimSuffix(rf, approvaltest.ReceivedSuffix) + approvaltest.ApprovedSuffix

var approved, received interface{}
if err := decodeJSONFile(rf, &received); err != nil {
fmt.Println("Could not create diff ", err)
return 3
}
if err := decodeJSONFile(af, &approved); err != nil && !os.IsNotExist(err) {
fmt.Println("Could not create diff ", err)
return 3
}

diff := cmp.Diff(approved, received)
added := color.New(color.FgBlack, color.BgGreen).SprintFunc()
deleted := color.New(color.FgBlack, color.BgRed).SprintFunc()
scanner := bufio.NewScanner(strings.NewReader(diff))
for scanner.Scan() {
line := scanner.Text()
if len(line) > 0 {
switch line[0] {
case '-':
line = deleted(line)
case '+':
line = added(line)
}
}
fmt.Println(line)
}

fmt.Println(rf)
fmt.Println("\nApprove Changes? (y/n)")
reader := bufio.NewReader(os.Stdin)
input, _, _ := reader.ReadRune()
switch input {
case 'y':
approvedPath := strings.Replace(rf, approvaltest.ReceivedSuffix, approvaltest.ApprovedSuffix, 1)
os.Rename(rf, approvedPath)
}
}
return 0
}

func findFiles(rootDir string, suffix string) []string {
files := []string{}
filepath.Walk(rootDir, func(path string, _ os.FileInfo, _ error) error {
if strings.HasSuffix(path, suffix) {
files = append(files, path)
}
return nil
})
return files
}

func decodeJSONFile(path string, out interface{}) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&out); err != nil {
return fmt.Errorf("cannot unmarshal file %q: %w", path, err)
}
return nil
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ go 1.20

require (
github.com/elastic/go-elasticsearch/v8 v8.8.1
github.com/fatih/color v1.15.0
github.com/gofrs/flock v0.8.1
github.com/google/go-cmp v0.5.9
github.com/tidwall/gjson v1.14.4
github.com/tidwall/sjson v1.2.5
github.com/urfave/cli/v3 v3.0.0-alpha4
)

require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,40 @@ github.com/elastic/elastic-transport-go/v8 v8.3.0 h1:DJGxovyQLXGr62e9nDMPSxRyWIO
github.com/elastic/elastic-transport-go/v8 v8.3.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI=
github.com/elastic/go-elasticsearch/v8 v8.8.1 h1:/OiP5Yex40q5eWpzFVQIS8jRE7SaEZrFkG9JbE6TXtY=
github.com/elastic/go-elasticsearch/v8 v8.8.1/go.mod h1:GU1BJHO7WeamP7UhuElYwzzHtvf9SDmeVpSSy9+o6Qg=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/urfave/cli/v3 v3.0.0-alpha4 h1:RJFGIs3mcalmc2YgliDh0Pa4l79S+Dqdz7cW8Fcp7Rg=
github.com/urfave/cli/v3 v3.0.0-alpha4/go.mod h1:ZFqSEHhze0duJACOdz43I5IcnKhf4RoTlOoUMBUggOI=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
Expand Down
140 changes: 140 additions & 0 deletions pkg/approvaltest/approvals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

// Package approvaltest contains helper functions to compare and assert
// the received content of a test vs the accepted.
package approvaltest

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

const (
// ApprovedSuffix signals a file has been reviewed and approved.
ApprovedSuffix = ".approved.json"

// ReceivedSuffix signals a file has changed and not yet been approved.
ReceivedSuffix = ".received.json"
)

// ApproveEventDocs compares the given event documents with
// the contents of the file in "<name>.approved.json".
//
// Any specified dynamic fields (e.g. @timestamp, observer.id)
// will be replaced with a static string for comparison.
//
// If the events differ, then the test will fail.
func ApproveEventDocs(t testing.TB, name string, eventDocs [][]byte, dynamic ...string) {
t.Helper()

// Rewrite all dynamic fields to have a known value,
// so dynamic fields don't affect diffs.
events := make([]interface{}, len(eventDocs))
for i, doc := range eventDocs {
for _, field := range dynamic {
existing := gjson.GetBytes(doc, field)
if !existing.Exists() {
continue
}

var err error
doc, err = sjson.SetBytes(doc, field, "dynamic")
if err != nil {
t.Fatal(err)
}
}

var event map[string]interface{}
if err := json.Unmarshal(doc, &event); err != nil {
t.Fatal(err)
}
events[i] = event
}

received := map[string]interface{}{"events": events}
approve(t, name, received)
}

// approve compares the given value with the contents of the file
// "<name>.approved.json".
//
// If the value differs, then the test will fail.
func approve(t testing.TB, name string, received interface{}) {
t.Helper()

var approved interface{}
if err := readApproved(name, &approved); err != nil {
t.Fatalf("failed to read approved file: %v", err)
}
if diff := cmp.Diff(approved, received); diff != "" {
if err := writeReceived(name, received); err != nil {
t.Fatalf("failed to write received file: %v", err)
}
t.Fatalf("%s\n%s\n\n", diff,
"Test failed. Run `make check-approvals` to verify the diff.",
)
} else {
// Remove an old *.received.json file if it exists, ignore errors
_ = removeReceived(name)
}
}

func readApproved(name string, approved interface{}) error {
path := name + ApprovedSuffix
f, err := os.Open(path)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to open approved file for %s: %w", name, err)
}
defer f.Close()
if os.IsNotExist(err) {
return nil
}
if err := json.NewDecoder(f).Decode(&approved); err != nil {
return fmt.Errorf("failed to decode approved file for %s: %w", name, err)
}
return nil
}

func removeReceived(name string) error {
return os.Remove(name + ReceivedSuffix)
}

func writeReceived(name string, received interface{}) error {
fullpath := name + ReceivedSuffix
if err := os.MkdirAll(filepath.Dir(fullpath), 0755); err != nil {
return fmt.Errorf("failed to create directories for received file: %w", err)
}
f, err := os.Create(fullpath)
if err != nil {
return fmt.Errorf("failed to create received file for %s: %w", name, err)
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(received); err != nil {
return fmt.Errorf("failed to encode received file for %s: %w", name, err)
}
return nil
}
Loading

0 comments on commit 2700b8f

Please sign in to comment.