Task: Simplify development

The base directory has all shell scripts in scripts/
and all example/test files in examples/.
A Makefile provides all the commands with helpful information.
If a developer simply types `make` then vendor is properly updated,
the code is formatted, linted, tested, built, acceptance test run,
and installed.

Linting errors resolved.
Ignored test case (`TestParsePath`) updated to work as expected.
This commit is contained in:
kenjones 2017-09-20 19:40:33 -04:00 committed by Mike Farah
parent 1ed8e7017e
commit 86639acf70
33 changed files with 351 additions and 53 deletions

3
.gitignore vendored
View File

@ -6,6 +6,7 @@
# Folders
_obj
_test
bin
build
.DS_Store
@ -26,3 +27,5 @@ coverage.out
*.prof
yaml
vendor/*/
tmp/
cover/

View File

@ -1,4 +1,6 @@
language: go
go:
- 1.7.x
script: ./ci.sh
script:
- scripts/devtools.sh
- make local build

9
Dockerfile.dev Normal file
View File

@ -0,0 +1,9 @@
FROM golang:1.7
COPY scripts/devtools.sh /opt/devtools.sh
RUN set -e -x \
&& /opt/devtools.sh
ENV CGO_ENABLED 0
ENV GOPATH /go:/yaml

106
Makefile Normal file
View File

@ -0,0 +1,106 @@
MAKEFLAGS += --warn-undefined-variables
SHELL := /bin/bash
.SHELLFLAGS := -o pipefail -euc
.DEFAULT_GOAL := install
include Makefile.variables
.PHONY: help
help:
@echo 'Management commands for cicdtest:'
@echo
@echo 'Usage:'
@echo ' ## Develop / Test Commands'
@echo ' make build Build yaml binary.'
@echo ' make install Install yaml.'
@echo ' make xcompile Build cross-compiled binaries of yaml.'
@echo ' make vendor Install dependencies using govendor.'
@echo ' make format Run code formatter.'
@echo ' make check Run static code analysis (lint).'
@echo ' make test Run tests on project.'
@echo ' make cover Run tests and capture code coverage metrics on project.'
@echo ' make clean Clean the directory tree of produced artifacts.'
@echo
@echo ' ## Utility Commands'
@echo ' make setup Configures Minishfit/Docker directory mounts.'
@echo
.PHONY: clean
clean:
@rm -rf bin build cover *.out
veryclean: clean
rm -rf tmp
find vendor/* -maxdepth 0 -ignore_readdir_race -type d -exec rm -rf {} \;
## prefix before other make targets to run in your local dev environment
local: | quiet
@$(eval DOCKRUN= )
quiet: # this is silly but shuts up 'Nothing to be done for `local`'
@:
prepare: tmp/dev_image_id
tmp/dev_image_id: Dockerfile.dev scripts/devtools.sh
@[ -z "${DOCKRUN}" ] || mkdir -p tmp
@[ -z "${DOCKRUN}" ] || docker rmi -f ${DEV_IMAGE} > /dev/null 2>&1 || true
@[ -z "${DOCKRUN}" ] || docker build -t ${DEV_IMAGE} -f Dockerfile.dev .
@[ -z "${DOCKRUN}" ] || docker inspect -f "{{ .ID }}" ${DEV_IMAGE} > tmp/dev_image_id
# ----------------------------------------------
# build
.PHONY: build
build: build/dev
.PHONY: build/dev
build/dev: test *.go
@mkdir -p bin/
${DOCKRUN} go build -o bin/yaml --ldflags "$(LDFLAGS)"
${DOCKRUN} bash ./scripts/acceptance.sh
## Compile the project for multiple OS and Architectures.
xcompile: check
@rm -rf build/
@mkdir -p build
${DOCKRUN} bash ./scripts/xcompile.sh
@find build -type d -exec chmod 755 {} \; || :
@find build -type f -exec chmod 755 {} \; || :
.PHONY: install
install: build
${DOCKRUN} go install
# Each of the fetch should be an entry within vendor.json; not currently included within project
.PHONY: vendor
vendor: tmp/dev_image_id
${DOCKRUN} bash ./scripts/vendor.sh
# ----------------------------------------------
# develop and test
.PHONY: format
format: vendor
${DOCKRUN} bash ./scripts/format.sh
.PHONY: check
check: format
${DOCKRUN} bash ./scripts/check.sh
.PHONY: test
test: check
${DOCKRUN} bash ./scripts/test.sh
.PHONY: cover
cover: check
@rm -rf cover/
@mkdir -p cover
${DOCKRUN} bash ./scripts/coverage.sh
@find cover -type d -exec chmod 755 {} \; || :
@find cover -type f -exec chmod 644 {} \; || :
# ----------------------------------------------
# utilities
.PHONY: setup
setup:
@bash ./scripts/setup.sh

34
Makefile.variables Normal file
View File

@ -0,0 +1,34 @@
export PROJECT = yaml
IMPORT_PATH := github.com/mikefarah/${PROJECT}
export GIT_COMMIT = $(shell git rev-parse --short HEAD)
export GIT_DIRTY = $(shell test -n "$$(git status --porcelain)" && echo "+CHANGES" || true)
export GIT_DESCRIBE = $(shell git describe --tags --always)
LDFLAGS :=
# LDFLAGS += -X ${IMPORT_PATH}/${PROJECT}.Commit=${GIT_COMMIT}
# LDFLAGS += -X ${IMPORT_PATH}/${PROJECT}.Version=${GIT_DESCRIBE}
# Windows environment?
CYG_CHECK := $(shell hash cygpath 2>/dev/null && echo 1)
ifeq ($(CYG_CHECK),1)
VBOX_CHECK := $(shell hash VBoxManage 2>/dev/null && echo 1)
# Docker Toolbox (pre-Windows 10)
ifeq ($(VBOX_CHECK),1)
ROOT := /${PROJECT}
else
# Docker Windows
ROOT := $(shell cygpath -m -a "$(shell pwd)")
endif
else
# all non-windows environments
ROOT := $(shell pwd)
endif
DEV_IMAGE := ${PROJECT}_dev
DOCKRUN := docker run --rm \
-v ${ROOT}/vendor:/go/src \
-v ${ROOT}:/${PROJECT}/src/${IMPORT_PATH} \
-w /${PROJECT}/src/${IMPORT_PATH} \
${DEV_IMAGE}

View File

@ -44,8 +44,8 @@ Use "yaml [command] --help" for more information about a command.
```
## Contribute
1. run `govendor sync` [link](https://github.com/kardianos/govendor)
1. `make vendor` OR run `govendor sync` [link](https://github.com/kardianos/govendor)
2. add unit tests
3. make changes
4. run ./precheckin.sh
3. apply changes
4. `make`
5. profit

View File

@ -1,3 +0,0 @@
#!/bin/bash
go test -coverprofile=coverage.out && go tool cover -html=coverage.out

View File

@ -1,8 +1,9 @@
package main
import (
"gopkg.in/yaml.v2"
"strconv"
yaml "gopkg.in/yaml.v2"
)
func entryInSlice(context yaml.MapSlice, key interface{}) *yaml.MapItem {
@ -40,7 +41,7 @@ func writeMap(context interface{}, paths []string, value interface{}) yaml.MapSl
log.Debugf("\tchild.Value %v\n", child.Value)
remainingPaths := paths[1:len(paths)]
remainingPaths := paths[1:]
child.Value = updatedChildValue(child.Value, remainingPaths, value)
log.Debugf("\tReturning mapSlice %v\n", mapSlice)
return mapSlice
@ -89,7 +90,7 @@ func writeArray(context interface{}, paths []string, value interface{}) []interf
log.Debugf("\tcurrentChild %v\n", currentChild)
remainingPaths := paths[1:len(paths)]
remainingPaths := paths[1:]
array[index] = updatedChildValue(currentChild, remainingPaths, value)
log.Debugf("\tReturning array %v\n", array)
return array
@ -113,7 +114,7 @@ func readMapSplat(context yaml.MapSlice, tail []string) interface{} {
var i = 0
for _, entry := range context {
if len(tail) > 0 {
newArray[i] = recurse(entry.Value, tail[0], tail[1:len(tail)])
newArray[i] = recurse(entry.Value, tail[0], tail[1:])
} else {
newArray[i] = entry.Value
}
@ -160,7 +161,7 @@ func readArraySplat(array []interface{}, tail []string) interface{} {
func calculateValue(value interface{}, tail []string) interface{} {
if len(tail) > 0 {
return recurse(value, tail[0], tail[1:len(tail)])
return recurse(value, tail[0], tail[1:])
}
return value
}

View File

@ -2,11 +2,12 @@ package main
import (
"fmt"
"github.com/op/go-logging"
"gopkg.in/yaml.v2"
"os"
"sort"
"testing"
logging "github.com/op/go-logging"
yaml "gopkg.in/yaml.v2"
)
func TestMain(m *testing.M) {

View File

@ -2,7 +2,8 @@ package main
import (
"encoding/json"
"gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v2"
)
func jsonToString(context interface{}) string {

View File

@ -16,13 +16,13 @@ func nextYamlPath(path string) (pathElement string, remaining string) {
switch path[0] {
case '[':
// e.g [0].blah.cat -> we need to return "0" and "blah.cat"
return search(path[1:len(path)], []uint8{']'}, true)
return search(path[1:], []uint8{']'}, true)
case '"':
// e.g "a.b".blah.cat -> we need to return "a.b" and "blah.cat"
return search(path[1:len(path)], []uint8{'"'}, true)
return search(path[1:], []uint8{'"'}, true)
default:
// e.g "a.blah.cat" -> return "a" and "blah.cat"
return search(path[0:len(path)], []uint8{'.', '['}, false)
return search(path[0:], []uint8{'.', '['}, false)
}
}
@ -39,7 +39,7 @@ func search(path string, matchingChars []uint8, skipNext bool) (pathElement stri
if remainingStart > len(path) {
remainingStart = len(path)
}
return path[0:i], path[remainingStart:len(path)]
return path[0:i], path[remainingStart:]
}
}
return path, ""

View File

@ -12,9 +12,9 @@ var parsePathsTests = []struct {
{"a.b[0]", []string{"a", "b", "0"}},
}
func testParsePath(t *testing.T) {
func TestParsePath(t *testing.T) {
for _, tt := range parsePathsTests {
assertResultWithContext(t, tt.expectedPaths, parsePath(tt.path), tt)
assertResultComplex(t, tt.expectedPaths, parsePath(tt.path))
}
}

View File

@ -1,9 +0,0 @@
#!/bin/bash
set -e
gofmt -w .
golint
./ci.sh
go install

View File

@ -2,14 +2,11 @@
set -e
go test
go build
# acceptance test
X=$(./yaml w sample.yaml b.c 3 | ./yaml r - b.c)
X=$(./bin/yaml w ./examples/sample.yaml b.c 3 | ./bin/yaml r - b.c)
if [ $X != 3 ]
then
echo "Failed acceptance test: expected 2 but was $X"
exit 1
fi
fi

26
scripts/check.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
set -o errexit
set -o pipefail
gometalinter \
--skip=examples \
--tests \
--vendor \
--disable=aligncheck \
--disable=gotype \
--disable=goconst \
--cyclo-over=20 \
--deadline=300s \
./...
gometalinter \
--skip=examples \
--tests \
--vendor \
--disable=aligncheck \
--disable=gotype \
--disable=goconst \
--disable=gocyclo \
--deadline=300s \
./...

6
scripts/coverage.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e
go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o cover/coverage.html

9
scripts/devtools.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
go get -u github.com/alecthomas/gometalinter
go get -u golang.org/x/tools/cmd/goimports
go get -u github.com/mitchellh/gox
go get -u github.com/kardianos/govendor
# install all the linters
gometalinter --install --update

3
scripts/format.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
find . \( -path ./vendor \) -prune -o -name "*.go" -exec goimports -w {} \;

92
scripts/setup.sh Executable file
View File

@ -0,0 +1,92 @@
#!/bin/bash
set -eu
find_mgr() {
if hash minishift 2>/dev/null; then
echo "minishift"
else
if hash docker-machine 2>/dev/null; then
echo "docker-machine"
fi
fi
}
get_vm_name() {
case "$1" in
minishift)
echo "minishift"
;;
docker-machine)
echo "${DOCKER_MACHINE_NAME}"
;;
*)
;;
esac
}
is_vm_running() {
local vm=$1
declare -a running=($(VBoxManage list runningvms | awk '{ print $1 }'))
local result='false'
for rvm in "${running[@]}"; do
if [[ "${rvm}" == *"${vm}"* ]]; then
result='true'
fi
done
echo "$result"
}
if hash cygpath 2>/dev/null; then
PROJECT_DIR=$(cygpath -w -a "$(pwd)")
else
PROJECT_DIR=$(pwd)
fi
VM_MGR=$(find_mgr)
if [[ -z $VM_MGR ]]; then
echo "ERROR: No VM Manager found; expected one of ['minishift', 'docker-machine']"
exit 1
fi
VM_NAME=$(get_vm_name "$VM_MGR")
if [[ -z $VM_NAME ]]; then
echo "ERROR: No VM found; try running 'eval $(docker-machine env)'"
exit 1
fi
if ! hash VBoxManage 2>/dev/null; then
echo "VirtualBox executable 'VBoxManage' not found in path"
exit 1
fi
avail=$(is_vm_running "$VM_NAME")
if [[ "$avail" == *"true"* ]]; then
res=$(VBoxManage sharedfolder add "${VM_NAME}" --name "${PROJECT}" --hostpath "${PROJECT_DIR}" --transient 2>&1)
if [[ -z $res || $res == *"already exists"* ]]; then
# no need to show that it already exists
:
else
echo "$res"
exit 1
fi
echo "VM: [${VM_NAME}] -- Added Sharedfolder [${PROJECT}] @Path [${PROJECT_DIR}]"
else
echo "$VM_NAME is not currently running; please start your VM and try again."
exit 1
fi
SSH_CMD="sudo mkdir -p /${PROJECT} ; sudo mount -t vboxsf ${PROJECT} /${PROJECT}"
case "${VM_MGR}" in
minishift)
minishift ssh "${SSH_CMD}"
echo "VM: [${VM_NAME}] -- Mounted Sharedfolder [${PROJECT}] @VM Path [/${PROJECT}]"
;;
docker-machine)
docker-machine ssh "${VM_NAME}" "${SSH_CMD}"
echo "VM: [${VM_NAME}] -- Mounted Sharedfolder [${PROJECT}] @VM Path [/${PROJECT}]"
;;
*)
;;
esac

3
scripts/test.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
go test -v $(go list ./... | grep -v -E 'vendor|examples')

8
scripts/vendor.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -e
govendor fetch github.com/op/go-logging
govendor fetch github.com/spf13/cobra
govendor fetch gopkg.in/yaml.v2
govendor sync

View File

@ -2,16 +2,18 @@ package main
import (
"fmt"
"gopkg.in/yaml.v2"
"os"
"reflect"
"testing"
yaml "gopkg.in/yaml.v2"
)
func parseData(rawData string) yaml.MapSlice {
var parsedData yaml.MapSlice
err := yaml.Unmarshal([]byte(rawData), &parsedData)
if err != nil {
fmt.Println("Error parsing yaml: %v", err)
fmt.Printf("Error parsing yaml: %v\n", err)
os.Exit(1)
}
return parsedData
@ -23,6 +25,12 @@ func assertResult(t *testing.T, expectedValue interface{}, actualValue interface
}
}
func assertResultComplex(t *testing.T, expectedValue interface{}, actualValue interface{}) {
if !reflect.DeepEqual(expectedValue, actualValue) {
t.Error("Expected <", expectedValue, "> but got <", actualValue, ">", fmt.Sprintf("%T", actualValue))
}
}
func assertResultWithContext(t *testing.T, expectedValue interface{}, actualValue interface{}, context interface{}) {
if expectedValue != actualValue {

15
yaml.go
View File

@ -2,13 +2,14 @@ package main
import (
"fmt"
"github.com/op/go-logging"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"strconv"
"strings"
logging "github.com/op/go-logging"
"github.com/spf13/cobra"
yaml "gopkg.in/yaml.v2"
)
var trimOutput = true
@ -37,7 +38,7 @@ func main() {
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
rootCmd.AddCommand(cmdRead, cmdWrite, cmdNew)
rootCmd.Execute()
_ = rootCmd.Execute()
}
func createReadCmd() *cobra.Command {
@ -138,7 +139,7 @@ func read(args []string) interface{} {
var paths = parsePath(path)
return readMap(parsedData, paths[0], paths[1:len(paths)])
return readMap(parsedData, paths[0], paths[1:])
}
func newProperty(cmd *cobra.Command, args []string) {
@ -180,7 +181,7 @@ func writeProperty(cmd *cobra.Command, args []string) {
}
updatedData := updateYaml(args)
if writeInplace {
ioutil.WriteFile(args[0], []byte(yamlToString(updatedData)), 0644)
_ = ioutil.WriteFile(args[0], []byte(yamlToString(updatedData)), 0644)
} else {
print(updatedData)
}
@ -288,7 +289,7 @@ func readData(filename string, parsedData interface{}, readAsJSON bool) error {
rawData = readFile(filename)
}
return yaml.Unmarshal([]byte(rawData), parsedData)
return yaml.Unmarshal(rawData, parsedData)
}
func readStdin() []byte {

View File

@ -24,22 +24,22 @@ func TestParseValue(t *testing.T) {
}
func TestRead(t *testing.T) {
result := read([]string{"sample.yaml", "b.c"})
result := read([]string{"examples/sample.yaml", "b.c"})
assertResult(t, 2, result)
}
func TestReadArray(t *testing.T) {
result := read([]string{"sample_array.yaml", "[1]"})
result := read([]string{"examples/sample_array.yaml", "[1]"})
assertResult(t, 2, result)
}
func TestReadString(t *testing.T) {
result := read([]string{"sample_text.yaml"})
result := read([]string{"examples/sample_text.yaml"})
assertResult(t, "hi", result)
}
func TestOrder(t *testing.T) {
result := read([]string{"order.yaml"})
result := read([]string{"examples/order.yaml"})
formattedResult := yamlToString(result)
assertResult(t,
`version: 3
@ -64,7 +64,7 @@ func TestNewYamlArray(t *testing.T) {
}
func TestUpdateYaml(t *testing.T) {
result := updateYaml([]string{"sample.yaml", "b.c", "3"})
result := updateYaml([]string{"examples/sample.yaml", "b.c", "3"})
formattedResult := fmt.Sprintf("%v", result)
assertResult(t,
"[{a Easy! as one two three} {b [{c 3} {d [3 4]} {e [[{name fred} {value 3}] [{name sam} {value 4}]]}]}]",
@ -72,7 +72,7 @@ func TestUpdateYaml(t *testing.T) {
}
func TestUpdateYamlArray(t *testing.T) {
result := updateYaml([]string{"sample_array.yaml", "[0]", "3"})
result := updateYaml([]string{"examples/sample_array.yaml", "[0]", "3"})
formattedResult := fmt.Sprintf("%v", result)
assertResult(t,
"[3 2 3]",
@ -80,12 +80,12 @@ func TestUpdateYamlArray(t *testing.T) {
}
func TestUpdateYaml_WithScript(t *testing.T) {
writeScript = "instruction_sample.yaml"
updateYaml([]string{"sample.yaml"})
writeScript = "examples/instruction_sample.yaml"
updateYaml([]string{"examples/sample.yaml"})
}
func TestNewYaml_WithScript(t *testing.T) {
writeScript = "instruction_sample.yaml"
writeScript = "examples/instruction_sample.yaml"
expectedResult := `b:
c: cat
e: