From fde25a009d6fc8756e6bc6823e80da2d5e60bfa2 Mon Sep 17 00:00:00 2001 From: Madhusudan Ravi Date: Sun, 17 Jul 2022 14:58:43 -0400 Subject: [PATCH] initial commit --- .clingy.yaml | 17 ++++++ .dockerignore | 5 ++ .github/CODEOWNERS | 1 + .github/dependabot.yaml | 20 +++++++ .github/workflows/lint.yaml | 15 ++++++ .github/workflows/release.yaml | 26 +++++++++ .github/workflows/semver.yaml | 41 ++++++++++++++ .github/workflows/test.yaml | 14 +++++ .gitignore | 30 +++++++++++ Dockerfile | 29 ++++++++++ LICENSE | 9 ++++ Makefile | 53 ++++++++++++++++++ build/.gitkeep | 0 cmd/clean.go | 35 ++++++++++++ cmd/init.go | 18 +++++++ cmd/root.go | 87 ++++++++++++++++++++++++++++++ cmd/run.go | 43 +++++++++++++++ cmd/test_data/basic_flow_01.yaml | 8 +++ cmd/validate.go | 35 ++++++++++++ cmd/version.go | 17 ++++++ docs/examples/.gitkeep | 0 go.mod | 13 +++++ go.sum | 13 +++++ internal/logger.go | 39 ++++++++++++++ internal/magick.go | 93 ++++++++++++++++++++++++++++++++ internal/utils.go | 19 +++++++ lib/models.go | 15 ++++++ lib/utils.go | 21 ++++++++ lib/utils_test.go | 1 + main.go | 7 +++ output/.gitkeep | 0 readme.md | 37 +++++++++++++ 32 files changed, 761 insertions(+) create mode 100644 .clingy.yaml create mode 100644 .dockerignore create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yaml create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/semver.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 build/.gitkeep create mode 100644 cmd/clean.go create mode 100644 cmd/init.go create mode 100644 cmd/root.go create mode 100644 cmd/run.go create mode 100644 cmd/test_data/basic_flow_01.yaml create mode 100644 cmd/validate.go create mode 100644 cmd/version.go create mode 100644 docs/examples/.gitkeep create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/logger.go create mode 100644 internal/magick.go create mode 100644 internal/utils.go create mode 100644 lib/models.go create mode 100644 lib/utils.go create mode 100644 lib/utils_test.go create mode 100644 main.go create mode 100644 output/.gitkeep create mode 100644 readme.md diff --git a/.clingy.yaml b/.clingy.yaml new file mode 100644 index 0000000..40571e9 --- /dev/null +++ b/.clingy.yaml @@ -0,0 +1,17 @@ +label: clingy flow +steps: +- label: start + description: starting clingy flow + command: echo -e "\033[34mStarting\033[0m" +- label: build clingy + description: building clingy with Makefile target + command: make build +- label: clingy init + description: displaying printout of only calling clingy + command: build/clingy -d +- label: clingy help + description: display help text for clingy + command: build/clingy --help -d +- label: finish + description: finished clingy flow + command: echo -e "\033[92mComplete\033[0m" \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f9a3239 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.github +.idea +*.iml +build +images diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d28fb22 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +@madhuravius \ No newline at end of file diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..28e1960 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + labels: + - dependencies + - actions + - Skip Changelog + schedule: + interval: weekly + day: sunday + - package-ecosystem: gomod + directory: / + labels: + - dependencies + - go + - Skip Changelog + schedule: + interval: weekly + day: sunday diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..4b658af --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,15 @@ +name: Lint + +on: + push: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: '>=1.18.0' + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..cca5afe --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,26 @@ +name: Go Releaser +# Source: https://github.com/tchupp/actions-update-semver-tags + +on: + release: + types: + - published + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v3 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v3 + if: startsWith(github.ref, 'refs/tags/') + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/semver.yaml b/.github/workflows/semver.yaml new file mode 100644 index 0000000..166cd54 --- /dev/null +++ b/.github/workflows/semver.yaml @@ -0,0 +1,41 @@ +name: Semantic Versioning of Tags +# Source: https://github.com/tchupp/actions-update-semver-tags + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + release: + types: + - published + +jobs: + update-semver-tags: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Update Previous Tags + shell: bash + run: | + release_sha="${GITHUB_SHA}" + git_ref="${GITHUB_REF}" + git_ref_type=$(echo "${git_ref}" | cut -d '/' -f 2) + if [[ "${git_ref_type}" != "tags" ]]; then + echo "Action should only run for 'tags' refs, was: '${git_ref}'" + exit 0 + fi + git_ref=$(echo "${git_ref}" | cut -d '/' -f 3-) + match="v[0-9]+.[0-9]+.[0-9]+" + if ! [[ "${git_ref}" =~ $match ]]; then + echo "Action should only run for tags that match the regex '$match', was: '${git_ref}'" + exit 0 + fi + prefix=$(echo "${git_ref}" | sed -E 's/([^0-9]*)([0-9]*)\.([0-9]*)\.([0-9]*)/\1/') + major=$(echo "${git_ref}" | sed -E 's/([^0-9]*)([0-9]*)\.([0-9]*)\.([0-9]*)/\2/') + minor=$(echo "${git_ref}" | sed -E 's/([^0-9]*)([0-9]*)\.([0-9]*)\.([0-9]*)/\3/') + patch=$(echo "${git_ref}" | sed -E 's/([^0-9]*)([0-9]*)\.([0-9]*)\.([0-9]*)/\4/') + git tag -f "${prefix}${major}" "${release_sha}" + git tag -f "${prefix}${major}.${minor}" "${release_sha}" + git push --tags -f \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..187be43 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,14 @@ +name: Test + +on: + push: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: '>=1.18.0' + - run: go test ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..529876c --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +.idea +*.iml + +output/* +!output/.gitkeep + +build/* +!build/.gitkeep \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..490efd1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# 1 - generate a new binary for use +FROM golang:1-bullseye as build +RUN apt-get update && \ + apt-get install wget +RUN wget https://github.com/ImageMagick/ImageMagick/releases/download/7.1.0.43/ImageMagick--gcc-x86_64.AppImage && \ + mv ImageMagick--gcc-x86_64.AppImage magick &&\ + chmod +x magick && \ + mv magick /usr/bin/ +RUN mkdir /opt/app +WORKDIR /opt/app +COPY . . +RUN make build + +# 2 - use the newly built image +FROM debian:bullseye +RUN apt-get update && \ + apt-get install -qqy x11-apps && \ + rm -rf /var/lib/apt/lists/* +ENV DISPLAY :0 +COPY --from=build /usr/bin/magick /usr/bin +COPY --from=build /opt/app/build/clingy /usr/bin +RUN groupadd --gid 1001 clingy && \ + useradd --uid 1001 --gid 1001 -m clingy && \ + usermod -a -G users clingy && \ + chown -R clingy /home/clingy +USER clingy +RUN mkdir /home/clingy/images +WORKDIR /home/clingy +ENTRYPOINT ["/usr/local/bin/clingy"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6ac0486 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright 2022 Madhusudan Ravi + +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. + +Taken from: https://opensource.org/licenses/MIT \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1509cbc --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +init: + go mod download +.PHONY: init + +build: + go build -o build/clingy +.PHONY: build + +start: + go run main.go +.PHONY: start + +clean: + rm build/clingy +.PHONY: clean + +lint: + docker run \ + --rm \ + -v $(shell pwd):/app \ + -w /app \ + golangci/golangci-lint:v1.46 \ + golangci-lint run +.PHONY: lint + +test: lint + go test ./... +.PHONY: test + +pretty: + go fmt ./... +.PHONY: pretty + +release: + @echo "don't forget to create and push a git tag! e.g." + @echo " git tag -a v0.1.0 -m 'First release'" + @echo " git push origin v0.1.0" + @sleep 3 + goreleaser release +.PHONY: release + +build-docker: + docker build -t clingy . + +run-docker: build-docker + # See readme on usage + xhost local:root + docker run \ + -e DISPLAY=${DISPLAY} \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + -v ${PWD}/.clingy.yaml:/home/clingy/.clingy.yaml \ + clingy +.PHONY: build-docker run-docker \ No newline at end of file diff --git a/build/.gitkeep b/build/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/cmd/clean.go b/cmd/clean.go new file mode 100644 index 0000000..4bb14a1 --- /dev/null +++ b/cmd/clean.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "io/ioutil" + "os" + "path" + + "github.com/spf13/cobra" +) + +// cleanCmd - clean temporary paths +var cleanCmd = &cobra.Command{ + Use: "clean", + Short: "Clean clingy", + PreRun: initRunWithoutArtifactDirectoryCreate, + Run: func(cmd *cobra.Command, args []string) { + logger.Println("Cleaning clingy generated files") + + subPaths, err := ioutil.ReadDir(getOutputPath()) + if err != nil { + logger.Println("Unable to read build directory for cleaning", err) + os.Exit(1) + } + for _, subPath := range subPaths { + if subPath.Name() == ".gitkeep" { + continue + } + err := os.RemoveAll(path.Join([]string{getOutputPath(), subPath.Name()}...)) + if err != nil { + logger.Println("Unable to clean up normal build path", err) + os.Exit(1) + } + } + }, +} diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..7993cfc --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// initCmd - inits a .clingy.yaml for use in the current path +var initCmd = &cobra.Command{ + Use: "init", + Short: "instantiate a .clingy.yaml for use in the cwd", + PreRun: initRunWithoutArtifactDirectoryCreate, + Run: func(cmd *cobra.Command, args []string) { + // check current path to determine if needing to write file + + // if it doesn't exist, go ahead and write the default template + + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..9cf1085 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "strconv" + "time" + + "clingy/internal" + "github.com/spf13/cobra" +) + +var ( + // buildNumber - a way to distinguish between various builds within an output directory + buildNumber = strconv.Itoa(int(time.Now().Unix())) + // logger - logger for debugging reasons, init'ed and typically writes to file in output directory w/ build # + logger *log.Logger + // version - version of the app to spit out, currently manually set :( + version = "v0.1.0" + + // flags + // debug - enable verbose logging + debug bool + // outputPath - a location to dump artifacts/output as a result of a clingy run + outputPath = "./output" + // inputFile - a path that contains an input file to digest and run clingy against + inputFile = "./.clingy.yaml" +) + +// getOutputPath - a string that generates a union of an (dynamic) output path and build number for artifacts +func getOutputPath() string { + return fmt.Sprintf("%s/%s", outputPath, buildNumber) +} + +// initRunWithArtifactDirectoryCreate - use this when needing to create a destination directory (ex: `run`) +func initRunWithArtifactDirectoryCreate(_ *cobra.Command, _ []string) { + internal.InitDestinationDirectory(getOutputPath()) + logger = internal.InitLogger(getOutputPath(), debug) +} + +// initRunWithoutArtifactDirectoryCreate - use this function when not needing a generalized create except with debug +func initRunWithoutArtifactDirectoryCreate(_ *cobra.Command, _ []string) { + logger = internal.InitLogger(getOutputPath(), debug) + if debug { + internal.InitDestinationDirectory(getOutputPath()) + } +} + +// rootCmd - entrypoint for clingy app +var rootCmd = &cobra.Command{ + Use: "clingy", + Short: "clingy is a tool to test and capture CLI flows", + Long: `clingy is a tool to test and capture CLI flows.`, + PreRun: initRunWithoutArtifactDirectoryCreate, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + if err := cmd.Help(); err != nil { + logger.Println("Error when trying to print help.", err) + os.Exit(1) + } + os.Exit(0) + } + }, +} + +// Execute ... +func Execute() { + if err := rootCmd.Execute(); err != nil { + logger.Println("Error when trying to execute", err) + os.Exit(1) + } +} + +// init ... +func init() { + rootCmd.AddCommand(cleanCmd) + rootCmd.AddCommand(initCmd) + rootCmd.AddCommand(runCmd) + rootCmd.AddCommand(validateCmd) + rootCmd.AddCommand(versionCmd) + + rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "enable debug logs") + rootCmd.PersistentFlags().StringVarP(&outputPath, "outputPath", "o", outputPath, "build path that dumps outputs") + rootCmd.PersistentFlags().StringVarP(&inputFile, "inputFile", "i", inputFile, "inputFile representing a .clingy.yaml") + rootCmd.Flags().SortFlags = true +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..e1d7c47 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "fmt" + "os" + "strconv" + + "github.com/spf13/cobra" + + "clingy/internal" + "clingy/lib" +) + +// runCmd - actually run clingy +var runCmd = &cobra.Command{ + Use: "run", + Short: "Run clingy", + PreRun: initRunWithArtifactDirectoryCreate, + Run: func(cmd *cobra.Command, args []string) { + logger.Println("Running clingy") + + clingyData, err := lib.ParseClingyFile(inputFile) + if err != nil { + fmt.Println(fmt.Sprintf("Error in reading: %s", inputFile), err) + os.Exit(1) + } + fmt.Printf("Running: %s", clingyData.Label) + + for idx, step := range clingyData.Steps { + internal.ClearTerminal() + imagePath := internal.CaptureWindow(logger, getOutputPath(), step.Command, strconv.Itoa(idx)) + if step.Label != "" { + internal.AddLabelToImage(logger, step.Label, imagePath) + } + if step.Description != "" { + internal.AddDescriptionToImage(logger, step.Description, imagePath) + } + } + + internal.ClearTerminal() + fmt.Println("Completed clingy run.") + }, +} diff --git a/cmd/test_data/basic_flow_01.yaml b/cmd/test_data/basic_flow_01.yaml new file mode 100644 index 0000000..52710b8 --- /dev/null +++ b/cmd/test_data/basic_flow_01.yaml @@ -0,0 +1,8 @@ +label: "basic flow #1" +steps: + - label: start + command: echo "Starting" + - label: printing timestamp + command: time + - label: finish + command: echo "Finishing" \ No newline at end of file diff --git a/cmd/validate.go b/cmd/validate.go new file mode 100644 index 0000000..e7f041f --- /dev/null +++ b/cmd/validate.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "clingy/internal" + "clingy/lib" +) + +// validateCmd - Command to validate the yaml +var validateCmd = &cobra.Command{ + Use: "validate", + Short: "Validate a clingy.yml file", + PreRun: initRunWithoutArtifactDirectoryCreate, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Validating %s\n", inputFile) + + if err := internal.CheckMagickBinary(); err != nil { + fmt.Println("Error with magick binary", err) + os.Exit(1) + } + + _, err := lib.ParseClingyFile(inputFile) + if err != nil { + fmt.Println(fmt.Sprintf("Error in validating: %s", inputFile), err) + os.Exit(1) + } + + fmt.Println("Completed validation, looks good!") + os.Exit(0) + }, +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..8f9581a --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// versionCmd - simple version Command to print +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of clingy", + PreRun: initRunWithoutArtifactDirectoryCreate, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(version) + }, +} diff --git a/docs/examples/.gitkeep b/docs/examples/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9b5dfa5 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module clingy + +go 1.17 + +require ( + github.com/spf13/cobra v1.5.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3a25d71 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/logger.go b/internal/logger.go new file mode 100644 index 0000000..67d2bf5 --- /dev/null +++ b/internal/logger.go @@ -0,0 +1,39 @@ +package internal + +import ( + "fmt" + "io/ioutil" + "log" + "os" +) + +// InitDestinationDirectory - sets up a destination directory for reuse for images/logs +func InitDestinationDirectory(buildPath string) { + err := os.MkdirAll(buildPath, 0755) + if err != nil { + log.Println("Error setting up the build", err) + os.Exit(1) + } +} + +// InitLogger - inits a debug logger for use if needed +func InitLogger(buildPath string, debug bool) *log.Logger { + var logger *log.Logger + if debug { + file, err := os.OpenFile( + fmt.Sprintf("%s/logs.txt", buildPath), + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0666, + ) + if err != nil { + log.Fatal(err) + } + + logger = log.New(file, "", log.Ldate|log.Ltime|log.Lshortfile) + logger.Println("Starting in debug mode") + } else { + logger = log.Default() + logger.SetOutput(ioutil.Discard) + } + return logger +} diff --git a/internal/magick.go b/internal/magick.go new file mode 100644 index 0000000..0e82930 --- /dev/null +++ b/internal/magick.go @@ -0,0 +1,93 @@ +package internal + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" +) + +// CaptureWindow - captures the terminal window +func CaptureWindow(logger *log.Logger, buildDirectory string, command string, screenshotName string) string { + // typical execution path - magick import -window $WINDOWID ./images/{screenshot} + logger.Println("Taking screenshot", fmt.Sprintf("WINDOWID - %s", os.Getenv("WINDOWID"))) + expectedPath := fmt.Sprintf("%s/%s.jpg", buildDirectory, screenshotName) + logger.Println("Saving to path", expectedPath) + + fmt.Println("> ", command) + commandAndArgs := strings.Split(command, " ") + command, args := commandAndArgs[0], commandAndArgs[1:] + output, _ := exec.Command(command, args...).CombinedOutput() // always allow the command to possibly error + fmt.Println(string(output)) + logger.Println("Finished executing command", command) + + imageCommand := exec.Command( + "magick", + "import", + "-window", + os.Getenv("WINDOWID"), + expectedPath, + ) + if err := imageCommand.Run(); err != nil { + log.Println("Error in capturing screenshot", err) + os.Exit(1) + } + + logger.Println("Saved to path", expectedPath) + return expectedPath +} + +// AddLabelToImage - add title text to image +func AddLabelToImage(logger *log.Logger, label string, imagePath string) { + // magick 0.jpg -font "FreeMono" -gravity South -pointsize 30 -fill "yellow" -annotate +0+100 'Caption' 0.jpg + imageCommand := exec.Command( + "magick", + imagePath, + "-font", + "FreeMono", + "-gravity", + "South", + "-pointsize", + "30", + "-fill", + "yellow", + "-annotate", + "+0+100", + label, + imagePath, + ) + output, err := imageCommand.CombinedOutput() + logger.Println("Combined output of label insertion", string(output)) + if err != nil { + os.Exit(1) + } + logger.Println("Saved label to image path", imagePath) +} + +// AddDescriptionToImage - add description text to image +func AddDescriptionToImage(logger *log.Logger, description string, imagePath string) { + // magick 0.jpg -font "FreeMono" -gravity South -pointsize 16 -fill "yellow" -annotate +0+60 'Description text. ' 0.jpg + imageCommand := exec.Command( + "magick", + imagePath, + "-font", + "FreeMono", + "-gravity", + "South", + "-pointsize", + "16", + "-fill", + "yellow", + "-annotate", + "+0+60", + description, + imagePath, + ) + output, err := imageCommand.CombinedOutput() + logger.Println("Combined output of description insertion", string(output)) + if err != nil { + os.Exit(1) + } + logger.Println("Saved description to image path", imagePath) +} diff --git a/internal/utils.go b/internal/utils.go new file mode 100644 index 0000000..08048d9 --- /dev/null +++ b/internal/utils.go @@ -0,0 +1,19 @@ +package internal + +import ( + "errors" + "fmt" + "os" + "os/exec" +) + +func CheckMagickBinary() error { + if _, err := exec.LookPath("magick"); os.IsNotExist(err) { + return errors.New("error: magick binary not found") + } + return nil +} + +func ClearTerminal() { + fmt.Print("\033[H\033[2J") // taken from: https://stackoverflow.com/a/22892171 +} diff --git a/lib/models.go b/lib/models.go new file mode 100644 index 0000000..ad70b38 --- /dev/null +++ b/lib/models.go @@ -0,0 +1,15 @@ +package lib + +// ClingyStep - step to execute on +type ClingyStep struct { + Label string `yaml:"label"` + Description string `yaml:"description"` + Command string `yaml:"command"` +} + +// ClingyTemplate - a full set of clingy instruction to follow +type ClingyTemplate struct { + Label string `yaml:"label"` + Description string `yaml:"description"` + Steps []ClingyStep `yaml:"steps"` +} diff --git a/lib/utils.go b/lib/utils.go new file mode 100644 index 0000000..762ac5f --- /dev/null +++ b/lib/utils.go @@ -0,0 +1,21 @@ +package lib + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +func ParseClingyFile(fileName string) (*ClingyTemplate, error) { + data, err := os.ReadFile(fileName) + if err != nil { + return nil, err + } + + var clingyData ClingyTemplate + if err = yaml.Unmarshal(data, &clingyData); err != nil { + return nil, err + } + + return &clingyData, nil +} diff --git a/lib/utils_test.go b/lib/utils_test.go new file mode 100644 index 0000000..55c21f8 --- /dev/null +++ b/lib/utils_test.go @@ -0,0 +1 @@ +package lib diff --git a/main.go b/main.go new file mode 100644 index 0000000..fc07831 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "clingy/cmd" + +func main() { + cmd.Execute() +} diff --git a/output/.gitkeep b/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..3835b49 --- /dev/null +++ b/readme.md @@ -0,0 +1,37 @@ +# clingy + +A CLI that helps you test other CLIs with end-to-end testing by capturing screenshots of commands in sequence, +so you don't have to. + +## TODO + +- [x] Take an input yaml and execute a set of commands in order +- [x] For each command execution, capture a screenshot of its output +- [x] Validate colorized support +- [x] Add basic label/description of each image +- [ ] Use [go bindings for imagemagick](https://github.com/gographics/imagick) instead of cmd exec +- [ ] Add examples +- [ ] Add multiline displaying text +- [ ] Add options to how Label/Description are displayed including placing text outside the body of an image +- [ ] Add docker example/support and validate +- [ ] Output clingy results to HTML with image/video references for easy reuse +- [ ] Add support for user interaction (keystroke entry) instead of only commands +- [ ] Waiters for waiting for an action to finish or anticipated text in payload +- [ ] Recordings + +## Requirements + +### Running natively + +Requires the following dependencies for this to even run: + +* [imagemagick](https://imagemagick.org/script/download.php) + +### Running in docker + +This can be run in docker. Instructions TBD + +## Misc / Credit + +Note - large parts of the organization and structure of this repo were pulled from +[this other repo](https://github.com/aptible/cloud-cli/).