with_clues 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Dockerfile.dx +28 -0
- data/README.md +14 -13
- data/bin/matrix +6 -0
- data/docker-compose.dx.yml +33 -0
- data/dx/build +60 -0
- data/dx/docker-compose.env +5 -0
- data/dx/dx.sh.lib +24 -0
- data/dx/exec +59 -0
- data/dx/prune +19 -0
- data/dx/setupkit.sh.lib +144 -0
- data/dx/show-help-in-app-container-then-wait.sh +38 -0
- data/dx/start +30 -0
- data/dx/stop +23 -0
- data/lib/with_clues/browser_logs.rb +1 -1
- data/lib/with_clues/html.rb +21 -8
- data/lib/with_clues/method.rb +10 -1
- data/lib/with_clues/private/custom_clue_method_analysis.rb +17 -7
- data/lib/with_clues/version.rb +1 -1
- data/with_clues.gemspec +1 -3
- metadata +17 -7
- data/.circleci/config.yml +0 -98
- data/bin/mk_circle_config +0 -84
- data/bin/mk_gem +0 -73
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b17d4876d0162d52c3cf3e767c70b26cb2d18ff9ed144eda164ddf3afdbcd6f7
|
4
|
+
data.tar.gz: 6e605bc07cae10efaf1b3866987f31cc36bd4bf619b5414b8d43d0f8c7b0c76a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 104eb1b602cd79c0dd3183daf5e47dff90e39bbeed82c06209b079902bc7e27d5a49631a22bc76c2a801a9193d3f8d8e0f026755962a2688fa135fb73aa9b426
|
7
|
+
data.tar.gz: 9c8bf899641fe07713725bd7ed7591ff85e59ad2e61c05b508ecddfc71f3a9c352132ed685e0f0cb3d16b304403c18f039626fa0a5e153d51002161ab66edab4
|
data/Dockerfile.dx
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
ARG RUBY_VERSION
|
2
|
+
FROM ruby:${RUBY_VERSION}
|
3
|
+
|
4
|
+
ENV DEBIAN_FRONTEND noninteractive
|
5
|
+
RUN apt-get -y update
|
6
|
+
|
7
|
+
|
8
|
+
# dx.snippet.start=templates/snippets/bundler/latest__bundler.dockerfile-snippet
|
9
|
+
# Based on documentation at https://guides.rubygems.org/command-reference/#gem-update
|
10
|
+
# based on the vendor's documentation
|
11
|
+
RUN echo "gem: --no-document" >> ~/.gemrc && \
|
12
|
+
gem update --system && \
|
13
|
+
gem install bundler
|
14
|
+
|
15
|
+
# dx.snippet.end=templates/snippets/bundler/latest__bundler.dockerfile-snippet
|
16
|
+
|
17
|
+
|
18
|
+
# dx.snippet.start=templates/snippets/vim/bullseye_vim.dockerfile-snippet
|
19
|
+
# Based on documentation at https://packages.debian.org/search?keywords=vim
|
20
|
+
# based on the vendor's documentation
|
21
|
+
ENV EDITOR=vim
|
22
|
+
RUN apt-get install -y vim && \
|
23
|
+
echo "set -o vi" >> /root/.bashrc
|
24
|
+
# dx.snippet.end=templates/snippets/vim/bullseye_vim.dockerfile-snippet
|
25
|
+
|
26
|
+
# This entrypoint produces a nice help message and waits around for you to do
|
27
|
+
# something with the container.
|
28
|
+
COPY dx/show-help-in-app-container-then-wait.sh /root
|
data/README.md
CHANGED
@@ -6,8 +6,7 @@ Suppose you have this:
|
|
6
6
|
expect(page).to have_content("My Awesome Site")
|
7
7
|
```
|
8
8
|
|
9
|
-
And Capybara says that that content is not there and that is all it says. You might slap in a `puts page.html` and try again.
|
10
|
-
Instead, what if you could not do that and do this?
|
9
|
+
And Capybara says that that content is not there and that is all it says. You might slap in a `puts page.html` and try again. Instead, what if you could not do that and do this?
|
11
10
|
|
12
11
|
```ruby
|
13
12
|
with_clues do
|
@@ -71,8 +70,7 @@ end
|
|
71
70
|
|
72
71
|
## Use
|
73
72
|
|
74
|
-
In general, you would not want to wrap all tests with `with_clues`. This is a diagnostic tool to allow you to get more
|
75
|
-
information on a test that is failing. As such, your workflow might be:
|
73
|
+
In general, you would not want to wrap all tests with `with_clues`. This is a diagnostic tool to allow you to get more information on a test that is failing. As such, your workflow might be:
|
76
74
|
|
77
75
|
1. Notice a test failing that you cannot easily diagnose
|
78
76
|
1. Wrap the failing assertion in `with_clues`:
|
@@ -89,8 +87,10 @@ information on a test that is failing. As such, your workflow might be:
|
|
89
87
|
|
90
88
|
There are three clues included:
|
91
89
|
|
92
|
-
* Dumping HTML - when `page` exists, it will dump the contents of `page.html`
|
93
|
-
|
90
|
+
* Dumping HTML - when `page` exists, it will dump the contents of `page.html` (for Selenium) or `page.content`
|
91
|
+
(for Playwright) when the test fails
|
92
|
+
* Dumping Browser logs - for a browser-based test, it will dump anything that was `console.log`'ed. This should
|
93
|
+
work with Selenium and Playwright
|
94
94
|
* Arbitrary context you pass in, for example when testing an Active Record
|
95
95
|
|
96
96
|
```ruby
|
@@ -107,15 +107,16 @@ There are three clues included:
|
|
107
107
|
`with_clues` is intended as a diagnostic tool you can develop and enhance over time. As your team writes more code or develops
|
108
108
|
more conventions, you can develop diagnostics as well.
|
109
109
|
|
110
|
-
To add one, create a class that implements `dump(notifier, context:)` or `dump(notifier, context:, page:)
|
110
|
+
To add one, create a class that implements `dump(notifier, context:)` or `dump(notifier, context:, page:)` or
|
111
|
+
`dump(notifier, context:, page:, captured_logs)`:
|
111
112
|
|
112
|
-
* `notifier` is a `WithClues::Notifier` that you should use to produce output:
|
113
|
-
* `notify` - output text, preceded with `[ with_clues ]` (this is so you can tell output from your code vs from `with_clues`)
|
114
|
-
* `blank_line` - a blank line (no prefix)
|
115
|
-
* `notify_raw` - output text without a prefix, useful for removing ambiguity about what is being output
|
113
|
+
* `notifier` is a `WithClues::Notifier` that you should use to produce output via the following methods:
|
114
|
+
* `notifier.notify` - output text, preceded with `[ with_clues ]` (this is so you can tell output from your code vs from `with_clues`)
|
115
|
+
* `notifier.blank_line` - a blank line (no prefix)
|
116
|
+
* `notifier.notify_raw` - output text without a prefix, useful for removing ambiguity about what is being output
|
116
117
|
* `context:` the context passed into `with_clues` (nil if it was omitted)
|
117
|
-
* `page:`
|
118
|
-
|
118
|
+
* `page:` will be given the Selenium or Playwright page object
|
119
|
+
* `captured_logs:` for Playwright, this will be the browser console logs captured inside the block
|
119
120
|
|
120
121
|
For example, suppose you want to output information about an Active Record like so:
|
121
122
|
|
data/bin/matrix
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# THIS IS GENERATED - DO NOT EDIT
|
2
|
+
|
3
|
+
services:
|
4
|
+
with-clues-3.1:
|
5
|
+
image: sustainable-rails/with-clues-dev:ruby-3.1
|
6
|
+
init: true
|
7
|
+
volumes:
|
8
|
+
- type: bind
|
9
|
+
source: "./"
|
10
|
+
target: "/root/work"
|
11
|
+
consistency: "consistent"
|
12
|
+
entrypoint: /root/show-help-in-app-container-then-wait.sh
|
13
|
+
working_dir: /root/work
|
14
|
+
with-clues-3.2:
|
15
|
+
image: sustainable-rails/with-clues-dev:ruby-3.2
|
16
|
+
init: true
|
17
|
+
volumes:
|
18
|
+
- type: bind
|
19
|
+
source: "./"
|
20
|
+
target: "/root/work"
|
21
|
+
consistency: "consistent"
|
22
|
+
entrypoint: /root/show-help-in-app-container-then-wait.sh
|
23
|
+
working_dir: /root/work
|
24
|
+
with-clues-3.3:
|
25
|
+
image: sustainable-rails/with-clues-dev:ruby-3.3
|
26
|
+
init: true
|
27
|
+
volumes:
|
28
|
+
- type: bind
|
29
|
+
source: "./"
|
30
|
+
target: "/root/work"
|
31
|
+
consistency: "consistent"
|
32
|
+
entrypoint: /root/show-help-in-app-container-then-wait.sh
|
33
|
+
working_dir: /root/work
|
data/dx/build
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
|
6
|
+
|
7
|
+
. "${SCRIPT_DIR}/dx.sh.lib"
|
8
|
+
|
9
|
+
require_command "docker"
|
10
|
+
load_docker_compose_env
|
11
|
+
|
12
|
+
usage_on_help "Builds the Docker image based on the Dockerfile" "" "build.pre" "build.post" "${@}"
|
13
|
+
|
14
|
+
for ruby_version in ${RUBY_VERSIONS[@]}; do
|
15
|
+
dockerfile="Dockerfile.dx"
|
16
|
+
docker_image_name="${IMAGE}:ruby-${ruby_version}"
|
17
|
+
|
18
|
+
log "Building for Ruby '${ruby_version}' using Docker image name '${docker_image_name}'"
|
19
|
+
|
20
|
+
exec_hook_if_exists "build.pre" "${dockerfile}" "${docker_image_name}"
|
21
|
+
|
22
|
+
docker build \
|
23
|
+
--file "${dockerfile}" \
|
24
|
+
--build-arg="RUBY_VERSION=${ruby_version}" \
|
25
|
+
--tag "${docker_image_name}" \
|
26
|
+
./
|
27
|
+
|
28
|
+
exec_hook_if_exists "build.post" "${dockerfile}" "${docker_image_name}"
|
29
|
+
log "🌈" "Your Docker image has been built tagged '${docker_image_name}'"
|
30
|
+
done
|
31
|
+
|
32
|
+
log "✅" "All images built"
|
33
|
+
|
34
|
+
log "✨" "Creating docker-compose.dx.yml"
|
35
|
+
compose_file="docker-compose.dx.yml"
|
36
|
+
log "🗑️" "Deleting previous ${compose_file}"
|
37
|
+
|
38
|
+
rm -f "${compose_file}"
|
39
|
+
echo "# THIS IS GENERATED - DO NOT EDIT" > "${compose_file}"
|
40
|
+
echo "" >> "${compose_file}"
|
41
|
+
echo "services:" >> "${compose_file}"
|
42
|
+
|
43
|
+
for ruby_version in ${RUBY_VERSIONS[@]}; do
|
44
|
+
log "Generating stanza for version '${ruby_version}'"
|
45
|
+
docker_image_name="${IMAGE}:ruby-${ruby_version}"
|
46
|
+
echo " with-clues-${ruby_version}:" >> "${compose_file}"
|
47
|
+
echo " image: ${docker_image_name}" >> "${compose_file}"
|
48
|
+
echo " init: true" >> "${compose_file}"
|
49
|
+
echo " volumes:" >> "${compose_file}"
|
50
|
+
echo " - type: bind" >> "${compose_file}"
|
51
|
+
echo " source: \"./\"" >> "${compose_file}"
|
52
|
+
echo " target: \"/root/work\"" >> "${compose_file}"
|
53
|
+
echo " consistency: \"consistent\"" >> "${compose_file}"
|
54
|
+
echo " entrypoint: /root/show-help-in-app-container-then-wait.sh" >> "${compose_file}"
|
55
|
+
echo " working_dir: /root/work" >> "${compose_file}"
|
56
|
+
done
|
57
|
+
log "🎼" "${compose_file} is now created"
|
58
|
+
log "🔄" "You can run dx/start to start it up, though you may need to stop it first with Ctrl-C"
|
59
|
+
|
60
|
+
# vim: ft=bash
|
data/dx/dx.sh.lib
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# shellcheck shell=bash
|
2
|
+
|
3
|
+
. "${SCRIPT_DIR}/setupkit.sh.lib"
|
4
|
+
|
5
|
+
require_command "realpath"
|
6
|
+
require_command "cat"
|
7
|
+
|
8
|
+
ENV_FILE=$(realpath "${SCRIPT_DIR}")/docker-compose.env
|
9
|
+
|
10
|
+
load_docker_compose_env() {
|
11
|
+
. "${ENV_FILE}"
|
12
|
+
}
|
13
|
+
|
14
|
+
exec_hook_if_exists() {
|
15
|
+
script_name=$1
|
16
|
+
shift
|
17
|
+
if [ -x "${SCRIPT_DIR}"/"${script_name}" ]; then
|
18
|
+
log "🪝" "${script_name} exists - executing"
|
19
|
+
"${SCRIPT_DIR}"/"${script_name}" "${@}"
|
20
|
+
else
|
21
|
+
debug "${script_name} does not exist"
|
22
|
+
fi
|
23
|
+
}
|
24
|
+
# vim: ft=bash
|
data/dx/exec
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
|
6
|
+
|
7
|
+
. "${SCRIPT_DIR}/dx.sh.lib"
|
8
|
+
|
9
|
+
require_command "docker"
|
10
|
+
load_docker_compose_env
|
11
|
+
|
12
|
+
usage_description="Execute a command inside the app's container."
|
13
|
+
usage_args="[-s service] [-v ruby_version] command"
|
14
|
+
usage_pre="exec.pre"
|
15
|
+
usage_on_help "${usage_description}" "${usage_args}" "${usage_pre}" "" "${@}"
|
16
|
+
|
17
|
+
LATEST_RUBY=${RUBY_VERSIONS[0]}
|
18
|
+
DEFAULT_SERVICE=with-clues-${LATEST_RUBY}
|
19
|
+
SERVICE="${SERVICE_NAME:-${DEFAULT_SERVICE}}"
|
20
|
+
while getopts "v:s:" opt "${@}"; do
|
21
|
+
case ${opt} in
|
22
|
+
v )
|
23
|
+
SERVICE="with-clues-${OPTARG}"
|
24
|
+
;;
|
25
|
+
s )
|
26
|
+
SERVICE="${OPTARG}"
|
27
|
+
;;
|
28
|
+
\? )
|
29
|
+
log "🛑" "Unknown option: ${opt}"
|
30
|
+
usage "${description}" "${usage_args}" "${usage_pre}"
|
31
|
+
;;
|
32
|
+
: )
|
33
|
+
log "🛑" "Invalid option: ${opt} requires an argument"
|
34
|
+
usage "${description}" "${usage_args}" "${usage_pre}"
|
35
|
+
;;
|
36
|
+
esac
|
37
|
+
done
|
38
|
+
shift $((OPTIND -1))
|
39
|
+
|
40
|
+
if [ $# -eq 0 ]; then
|
41
|
+
log "🛑" "You must provide a command e.g. bash or ls -l"
|
42
|
+
usage "${description}" "${usage_args}" "${usage_pre}"
|
43
|
+
fi
|
44
|
+
|
45
|
+
|
46
|
+
exec_hook_if_exists "exec.pre"
|
47
|
+
|
48
|
+
log "🚂" "Running '${*}' inside container with service name '${SERVICE}'"
|
49
|
+
|
50
|
+
docker \
|
51
|
+
compose \
|
52
|
+
--file docker-compose.dx.yaml \
|
53
|
+
--project-name "${PROJECT_NAME}" \
|
54
|
+
--env-file "${ENV_FILE}" \
|
55
|
+
exec \
|
56
|
+
"${SERVICE}" \
|
57
|
+
"${@}"
|
58
|
+
|
59
|
+
# vim: ft=bash
|
data/dx/prune
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
|
6
|
+
|
7
|
+
. "${SCRIPT_DIR}/dx.sh.lib"
|
8
|
+
require_command "docker"
|
9
|
+
load_docker_compose_env
|
10
|
+
|
11
|
+
usage_on_help "Prune containers for this repo" "" "" "" "${@}"
|
12
|
+
|
13
|
+
for container_id in $(docker container ls -a -f "name=^${PROJECT_NAME}-.*-1$" --format="{{.ID}}"); do
|
14
|
+
log "🗑" "Removing container with id '${container_id}'"
|
15
|
+
docker container rm "${container_id}"
|
16
|
+
done
|
17
|
+
echo "🧼" "Containers removed"
|
18
|
+
|
19
|
+
# vim: ft=bash
|
data/dx/setupkit.sh.lib
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# shellcheck shell=bash
|
2
|
+
|
3
|
+
fatal() {
|
4
|
+
remainder=${*:2}
|
5
|
+
if [ -z "$remainder" ]; then
|
6
|
+
log "🛑" "${@}"
|
7
|
+
else
|
8
|
+
log "${@}"
|
9
|
+
fi
|
10
|
+
exit 1
|
11
|
+
}
|
12
|
+
|
13
|
+
log() {
|
14
|
+
emoji=$1
|
15
|
+
remainder=${*:2}
|
16
|
+
if [ -z "${NO_EMOJI}" ]; then
|
17
|
+
echo "[ ${0} ] ${*}"
|
18
|
+
else
|
19
|
+
# if remainder is empty that means no emoji was passed
|
20
|
+
if [ -z "$remainder" ]; then
|
21
|
+
echo "[ ${0} ] ${*}"
|
22
|
+
else # emoji was passed, but we ignore it
|
23
|
+
echo "[ ${0} ] ${remainder}"
|
24
|
+
fi
|
25
|
+
fi
|
26
|
+
}
|
27
|
+
|
28
|
+
debug() {
|
29
|
+
message=$1
|
30
|
+
if [ -z "${DOCKBOX_DEBUG}" ]; then
|
31
|
+
return
|
32
|
+
fi
|
33
|
+
log "🐛" "${message}"
|
34
|
+
}
|
35
|
+
|
36
|
+
usage() {
|
37
|
+
description=$1
|
38
|
+
arg_names=$2
|
39
|
+
pre_hook=$3
|
40
|
+
post_hook=$4
|
41
|
+
echo "usage: ${0} [-h] ${arg_names}"
|
42
|
+
if [ -n "${description}" ]; then
|
43
|
+
echo
|
44
|
+
echo "DESCRIPTION"
|
45
|
+
echo " ${description}"
|
46
|
+
fi
|
47
|
+
if [ -n "${pre_hook}" ] || [ -n "${post_hook}" ]; then
|
48
|
+
echo
|
49
|
+
echo "HOOKS"
|
50
|
+
if [ -n "${pre_hook}" ]; then
|
51
|
+
echo " ${pre_hook} - if present, called before the main action"
|
52
|
+
fi
|
53
|
+
if [ -n "${post_hook}" ]; then
|
54
|
+
echo " ${post_hook} - if present, called after the main action"
|
55
|
+
fi
|
56
|
+
fi
|
57
|
+
exit 0
|
58
|
+
}
|
59
|
+
|
60
|
+
usage_on_help() {
|
61
|
+
description=$1
|
62
|
+
arg_names=$2
|
63
|
+
pre_hook=$3
|
64
|
+
post_hook=$4
|
65
|
+
# These are the args passed to the invocation so this
|
66
|
+
# function can determine if the user requested help
|
67
|
+
cli_args=( "${@:5}" )
|
68
|
+
|
69
|
+
for arg in "${cli_args[@]}"; do
|
70
|
+
if [ "${arg}" = "-h" ] || [ "${arg}" = "--help" ]; then
|
71
|
+
usage "${description}" "${arg_names}" "${pre_hook}" "${post_hook}"
|
72
|
+
fi
|
73
|
+
done
|
74
|
+
}
|
75
|
+
|
76
|
+
# Read user input into the variable 'INPUT'
|
77
|
+
#
|
78
|
+
# Args:
|
79
|
+
#
|
80
|
+
# [1] - an emoji to use for messages
|
81
|
+
# [2] - the message explaining what input is being requested
|
82
|
+
# [3] - a default value to use if no value is provided
|
83
|
+
#
|
84
|
+
# Respects NO_EMOJI when outputing messages to the user
|
85
|
+
user_input() {
|
86
|
+
emoji=$1
|
87
|
+
message=$2
|
88
|
+
default=$3
|
89
|
+
prompt=$4
|
90
|
+
|
91
|
+
if [ -z "$message" ]; then
|
92
|
+
echo "user_input requires a message"
|
93
|
+
exit 1
|
94
|
+
fi
|
95
|
+
|
96
|
+
INPUT=
|
97
|
+
|
98
|
+
if [ -z "${prompt}" ]; then
|
99
|
+
prompt=$(log "${emoji}" "Value: ")
|
100
|
+
if [ -n "${default}" ]; then
|
101
|
+
prompt=$(log "${emoji}" "Value (or hit return to use '${default}'): ")
|
102
|
+
fi
|
103
|
+
fi
|
104
|
+
|
105
|
+
while [ -z "${INPUT}" ]; do
|
106
|
+
|
107
|
+
log "$emoji" "$message"
|
108
|
+
read -r -p "${prompt}" INPUT
|
109
|
+
if [ -z "$INPUT" ]; then
|
110
|
+
INPUT=$default
|
111
|
+
fi
|
112
|
+
if [ -z "$INPUT" ]; then
|
113
|
+
log "😶", "You must provide a value"
|
114
|
+
fi
|
115
|
+
done
|
116
|
+
}
|
117
|
+
|
118
|
+
user_confirm() {
|
119
|
+
user_input "$1" "$2" "$3" "y/n> "
|
120
|
+
}
|
121
|
+
|
122
|
+
require_not_exist() {
|
123
|
+
file=$1
|
124
|
+
message=$2
|
125
|
+
if [ -e "${file}" ]; then
|
126
|
+
fatal "$message"
|
127
|
+
fi
|
128
|
+
}
|
129
|
+
require_exist() {
|
130
|
+
file=$1
|
131
|
+
message=$2
|
132
|
+
if [ ! -e "${file}" ]; then
|
133
|
+
fatal "$message"
|
134
|
+
fi
|
135
|
+
}
|
136
|
+
|
137
|
+
require_command() {
|
138
|
+
command_name=$1
|
139
|
+
if ! command -v "${command_name}" >/dev/null 2>&1; then
|
140
|
+
fatal "Command '${command_name}' not found - it is required for this script to run"
|
141
|
+
fi
|
142
|
+
}
|
143
|
+
|
144
|
+
# vim: ft=bash
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
# Ideally, the message below is shown after everything starts up. We can't
|
6
|
+
# achieve this using healtchecks because the interval for a healtcheck is
|
7
|
+
# also an initial delay, and we don't really want to do healthchecks on
|
8
|
+
# our DB or Redis every 2 seconds. So, we sleep just a bit to let
|
9
|
+
# the other containers start up and vomit out their output first.
|
10
|
+
sleep 2
|
11
|
+
# Output some helpful messaging when invoking `dx/start` (which itself is
|
12
|
+
# a convenience script for `docker compose up`.
|
13
|
+
#
|
14
|
+
# Adding this to work around the mild inconvenience of the `app` container's
|
15
|
+
# entrypoint generating no output.
|
16
|
+
#
|
17
|
+
cat <<-'PROMPT'
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
🎉 Dev Environment Initialized! 🎉
|
22
|
+
|
23
|
+
ℹ️ To use this environment, open a new terminal and run
|
24
|
+
|
25
|
+
dx/exec bash
|
26
|
+
|
27
|
+
🕹 Use `ctrl-c` to exit.
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
PROMPT
|
32
|
+
|
33
|
+
# Using `sleep infinity` instead of `tail -f /dev/null`. This may be a
|
34
|
+
# performance improvement based on the conversation on a semi-related
|
35
|
+
# StackOverflow page.
|
36
|
+
#
|
37
|
+
# @see https://stackoverflow.com/a/41655546
|
38
|
+
sleep infinity
|
data/dx/start
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
|
6
|
+
|
7
|
+
. "${SCRIPT_DIR}/dx.sh.lib"
|
8
|
+
require_command "docker"
|
9
|
+
load_docker_compose_env
|
10
|
+
|
11
|
+
usage_on_help "Starts all services, including a container in which to run your app" "" "" "" "${@}"
|
12
|
+
|
13
|
+
log "🚀" "Starting docker-compose.dx.yml"
|
14
|
+
|
15
|
+
BUILD=--build
|
16
|
+
if [ "${1}" == "--no-build" ]; then
|
17
|
+
BUILD=
|
18
|
+
fi
|
19
|
+
|
20
|
+
docker \
|
21
|
+
compose \
|
22
|
+
--file docker-compose.dx.yml \
|
23
|
+
--project-name "${PROJECT_NAME}" \
|
24
|
+
--env-file "${ENV_FILE}" \
|
25
|
+
up \
|
26
|
+
"${BUILD}" \
|
27
|
+
--timestamps \
|
28
|
+
--force-recreate
|
29
|
+
|
30
|
+
# vim: ft=bash
|
data/dx/stop
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
|
6
|
+
|
7
|
+
. "${SCRIPT_DIR}/dx.sh.lib"
|
8
|
+
require_command "docker"
|
9
|
+
load_docker_compose_env
|
10
|
+
|
11
|
+
usage_on_help "Stops all services, the container in which to run your app and removes any volumes" "" "" "" "${@}"
|
12
|
+
|
13
|
+
log "🚀" "Stopping docker-compose.dx.yml"
|
14
|
+
|
15
|
+
docker \
|
16
|
+
compose \
|
17
|
+
--file docker-compose.dx.yml \
|
18
|
+
--project-name "${PROJECT_NAME}" \
|
19
|
+
--env-file "${ENV_FILE}" \
|
20
|
+
down \
|
21
|
+
--volumes
|
22
|
+
|
23
|
+
# vim: ft=bash
|
data/lib/with_clues/html.rb
CHANGED
@@ -1,18 +1,31 @@
|
|
1
1
|
module WithClues
|
2
2
|
class Html
|
3
|
-
def dump(notifier, page:, context:)
|
3
|
+
def dump(notifier, page:, context:, captured_logs: [])
|
4
|
+
access_page_html = if page.respond_to?(:html)
|
5
|
+
->(page) { page.html }
|
6
|
+
elsif page.respond_to?(:content)
|
7
|
+
->(page) { page.content }
|
8
|
+
elsif page.respond_to?(:native)
|
9
|
+
->(page) { page.native }
|
10
|
+
else
|
11
|
+
notifier.notify "Something may be wrong. page (#{page.class}) does not respond to #html, #native, or #content"
|
12
|
+
return
|
13
|
+
end
|
4
14
|
notifier.blank_line
|
5
15
|
notifier.notify "HTML {"
|
6
16
|
notifier.blank_line
|
7
|
-
|
8
|
-
notifier.notify_raw page.html
|
9
|
-
elsif page.respond_to?(:native)
|
10
|
-
notifier.notify_raw page.native
|
11
|
-
else
|
12
|
-
notifier.notify "[!] Something may be wrong. page (#{page.class}) does not respond to #html or #native"
|
13
|
-
end
|
17
|
+
notifier.notify_raw access_page_html.(page)
|
14
18
|
notifier.blank_line
|
15
19
|
notifier.notify "} END HTML"
|
20
|
+
if captured_logs.any?
|
21
|
+
notifier.notify "LOGS {"
|
22
|
+
notifier.blank_line
|
23
|
+
captured_logs.each do |log|
|
24
|
+
notifier.notify_raw log
|
25
|
+
end
|
26
|
+
notifier.blank_line
|
27
|
+
notifier.notify "} END LOGS"
|
28
|
+
end
|
16
29
|
end
|
17
30
|
end
|
18
31
|
end
|
data/lib/with_clues/method.rb
CHANGED
@@ -17,6 +17,15 @@ module WithClues
|
|
17
17
|
# unexpectedly failing
|
18
18
|
def with_clues(context=nil, &block)
|
19
19
|
notifier = WithClues::Notifier.new($stdout)
|
20
|
+
captured_logs = []
|
21
|
+
if defined?(page) && page.respond_to?(:on)
|
22
|
+
begin
|
23
|
+
page.on("console", ->(msg) { captured_logs << msg.text })
|
24
|
+
rescue => ex
|
25
|
+
raise ex
|
26
|
+
notifier.notify "'page' was defined and responds to #on, however invoking it generated an exception: #{ex}"
|
27
|
+
end
|
28
|
+
end
|
20
29
|
block.()
|
21
30
|
notifier.notify "A passing test has been wrapped with `with_clues`. You should remove the call to `with_clues`"
|
22
31
|
rescue Exception => ex
|
@@ -27,7 +36,7 @@ module WithClues
|
|
27
36
|
if defined?(page)
|
28
37
|
notifier.notify "Test failed: #{ex.message}"
|
29
38
|
@@clue_classes[:require_page].each do |klass|
|
30
|
-
klass.new.dump(notifier, context: context, page: page)
|
39
|
+
klass.new.dump(notifier, context: context, page: page, captured_logs: captured_logs)
|
31
40
|
end
|
32
41
|
end
|
33
42
|
raise ex
|
@@ -14,15 +14,17 @@ module WithClues
|
|
14
14
|
|
15
15
|
return BadParams.new(two_arg_method.errors)
|
16
16
|
|
17
|
-
elsif params.size ==
|
18
|
-
three_arg_method =
|
17
|
+
elsif params.size == 4
|
18
|
+
three_arg_method = FourArgMethod.new(params)
|
19
19
|
if three_arg_method.valid?
|
20
20
|
return RequiresPageObject.new
|
21
21
|
end
|
22
22
|
return BadParams.new(three_arg_method.errors)
|
23
23
|
end
|
24
24
|
|
25
|
-
BadParams.new([
|
25
|
+
BadParams.new([
|
26
|
+
"dump (#{unbound_method.owner}) accepted #{params.size} arguments, not 2 or 4. Got: #{params.map(&:to_s).join(", ")}, should be one positional and either one keyword arg named 'context:' or three keyword args named 'context:', 'page:', and 'captured_logs:'"
|
27
|
+
])
|
26
28
|
end
|
27
29
|
|
28
30
|
def standard_implementation?
|
@@ -62,6 +64,9 @@ module WithClues
|
|
62
64
|
@name
|
63
65
|
end
|
64
66
|
end
|
67
|
+
def to_s
|
68
|
+
"#<#{self.class} #{name}/#{@type}"
|
69
|
+
end
|
65
70
|
end
|
66
71
|
|
67
72
|
class TwoArgMethod
|
@@ -84,7 +89,7 @@ module WithClues
|
|
84
89
|
@errors << "Param #{param_number}, #{param.name}, is not a required keyword param"
|
85
90
|
end
|
86
91
|
if !param.named?(*allowed_names)
|
87
|
-
@errors << "Param #{param_number}, #{param.name}, should be
|
92
|
+
@errors << "Param #{param_number}, #{param.name}, should be one of #{allowed_names.join(',')}"
|
88
93
|
end
|
89
94
|
end
|
90
95
|
|
@@ -93,14 +98,15 @@ module WithClues
|
|
93
98
|
end
|
94
99
|
end
|
95
100
|
|
96
|
-
class
|
101
|
+
class FourArgMethod < TwoArgMethod
|
97
102
|
def initialize(params)
|
98
103
|
super(params)
|
99
104
|
require_keyword(3,params[2])
|
105
|
+
require_keyword(4,params[3])
|
100
106
|
end
|
101
107
|
private
|
102
108
|
def allowed_names
|
103
|
-
[ :context, :page ]
|
109
|
+
[ :context, :page, :captured_logs ]
|
104
110
|
end
|
105
111
|
end
|
106
112
|
|
@@ -126,7 +132,11 @@ module WithClues
|
|
126
132
|
|
127
133
|
class BadParams < CustomClueMethodAnalysis
|
128
134
|
def initialize(errors)
|
129
|
-
|
135
|
+
if errors.empty?
|
136
|
+
raise ArgumentError,"BadParams requires errors"
|
137
|
+
else
|
138
|
+
@message = errors.map(&:to_s).join(", ")
|
139
|
+
end
|
130
140
|
end
|
131
141
|
|
132
142
|
DEFAULT_ERROR = "dump must take one required param, one keyword param named context: and an optional keyword param named page:"
|
data/lib/with_clues/version.rb
CHANGED
data/with_clues.gemspec
CHANGED
@@ -1,13 +1,11 @@
|
|
1
1
|
require_relative "lib/with_clues/version"
|
2
2
|
|
3
|
-
#require_relative "lib/«gem»/version"
|
4
|
-
|
5
3
|
Gem::Specification.new do |spec|
|
6
4
|
spec.name = "with_clues"
|
7
5
|
spec.version = WithClues::VERSION
|
8
6
|
spec.authors = ["Dave Copeland"]
|
9
7
|
spec.email = ["davec@naildrivin5.com"]
|
10
|
-
spec.summary = %q{
|
8
|
+
spec.summary = %q{Temporarily add context to failing tests to get more information, such as what HTML was being examined when a browser-based test fails.}
|
11
9
|
spec.homepage = "https://sustainable-rails.com"
|
12
10
|
spec.license = "Hippocratic"
|
13
11
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: with_clues
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dave Copeland
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -73,24 +73,33 @@ executables: []
|
|
73
73
|
extensions: []
|
74
74
|
extra_rdoc_files: []
|
75
75
|
files:
|
76
|
-
- ".circleci/config.yml"
|
77
76
|
- ".gitignore"
|
78
77
|
- ".rspec"
|
79
78
|
- ".tool-versions"
|
80
79
|
- CHANGELOG.md
|
81
80
|
- CODE_OF_CONDUCT.md
|
82
81
|
- CONTRIBUTING.md
|
82
|
+
- Dockerfile.dx
|
83
83
|
- Gemfile
|
84
84
|
- LICENSE.md
|
85
85
|
- README.md
|
86
86
|
- Rakefile
|
87
87
|
- bin/ci
|
88
88
|
- bin/console
|
89
|
-
- bin/
|
90
|
-
- bin/mk_gem
|
89
|
+
- bin/matrix
|
91
90
|
- bin/rake
|
92
91
|
- bin/rspec
|
93
92
|
- bin/setup
|
93
|
+
- docker-compose.dx.yml
|
94
|
+
- dx/build
|
95
|
+
- dx/docker-compose.env
|
96
|
+
- dx/dx.sh.lib
|
97
|
+
- dx/exec
|
98
|
+
- dx/prune
|
99
|
+
- dx/setupkit.sh.lib
|
100
|
+
- dx/show-help-in-app-container-then-wait.sh
|
101
|
+
- dx/start
|
102
|
+
- dx/stop
|
94
103
|
- lib/with_clues.rb
|
95
104
|
- lib/with_clues/browser_logs.rb
|
96
105
|
- lib/with_clues/html.rb
|
@@ -121,8 +130,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
130
|
- !ruby/object:Gem::Version
|
122
131
|
version: '0'
|
123
132
|
requirements: []
|
124
|
-
rubygems_version: 3.
|
133
|
+
rubygems_version: 3.5.21
|
125
134
|
signing_key:
|
126
135
|
specification_version: 4
|
127
|
-
summary:
|
136
|
+
summary: Temporarily add context to failing tests to get more information, such as
|
137
|
+
what HTML was being examined when a browser-based test fails.
|
128
138
|
test_files: []
|
data/.circleci/config.yml
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
# THIS IS GENERATED - DO NOT EDIT
|
2
|
-
# regenerate with bin/mk_circle_config
|
3
|
-
# You are very welcome
|
4
|
-
---
|
5
|
-
version: '2.1'
|
6
|
-
jobs:
|
7
|
-
ruby__2_6:
|
8
|
-
docker:
|
9
|
-
- image: cimg/ruby:2.6
|
10
|
-
steps:
|
11
|
-
- checkout
|
12
|
-
- run:
|
13
|
-
name: Setup for build
|
14
|
-
command: bin/setup
|
15
|
-
- run:
|
16
|
-
name: Ensure bin/setup is idempotent
|
17
|
-
command: bin/setup
|
18
|
-
- run:
|
19
|
-
name: Create the test results dir
|
20
|
-
command: mkdir -p /tmp/test-results/2.6
|
21
|
-
- run:
|
22
|
-
name: Run all tests
|
23
|
-
command: bin/ci /tmp/test-results/2.6/rspec_results.xml
|
24
|
-
- store_test_results:
|
25
|
-
path: "/tmp/test-results/2.6"
|
26
|
-
- store_artifacts:
|
27
|
-
path: "/tmp/test-results/2.6"
|
28
|
-
ruby__2_7:
|
29
|
-
docker:
|
30
|
-
- image: cimg/ruby:2.7
|
31
|
-
steps:
|
32
|
-
- checkout
|
33
|
-
- run:
|
34
|
-
name: Setup for build
|
35
|
-
command: bin/setup
|
36
|
-
- run:
|
37
|
-
name: Ensure bin/setup is idempotent
|
38
|
-
command: bin/setup
|
39
|
-
- run:
|
40
|
-
name: Create the test results dir
|
41
|
-
command: mkdir -p /tmp/test-results/2.7
|
42
|
-
- run:
|
43
|
-
name: Run all tests
|
44
|
-
command: bin/ci /tmp/test-results/2.7/rspec_results.xml
|
45
|
-
- store_test_results:
|
46
|
-
path: "/tmp/test-results/2.7"
|
47
|
-
- store_artifacts:
|
48
|
-
path: "/tmp/test-results/2.7"
|
49
|
-
ruby__3_0:
|
50
|
-
docker:
|
51
|
-
- image: cimg/ruby:3.0
|
52
|
-
steps:
|
53
|
-
- checkout
|
54
|
-
- run:
|
55
|
-
name: Setup for build
|
56
|
-
command: bin/setup
|
57
|
-
- run:
|
58
|
-
name: Ensure bin/setup is idempotent
|
59
|
-
command: bin/setup
|
60
|
-
- run:
|
61
|
-
name: Create the test results dir
|
62
|
-
command: mkdir -p /tmp/test-results/3.0
|
63
|
-
- run:
|
64
|
-
name: Run all tests
|
65
|
-
command: bin/ci /tmp/test-results/3.0/rspec_results.xml
|
66
|
-
- store_test_results:
|
67
|
-
path: "/tmp/test-results/3.0"
|
68
|
-
- store_artifacts:
|
69
|
-
path: "/tmp/test-results/3.0"
|
70
|
-
ruby__3_1:
|
71
|
-
docker:
|
72
|
-
- image: cimg/ruby:3.1
|
73
|
-
steps:
|
74
|
-
- checkout
|
75
|
-
- run:
|
76
|
-
name: Setup for build
|
77
|
-
command: bin/setup
|
78
|
-
- run:
|
79
|
-
name: Ensure bin/setup is idempotent
|
80
|
-
command: bin/setup
|
81
|
-
- run:
|
82
|
-
name: Create the test results dir
|
83
|
-
command: mkdir -p /tmp/test-results/3.1
|
84
|
-
- run:
|
85
|
-
name: Run all tests
|
86
|
-
command: bin/ci /tmp/test-results/3.1/rspec_results.xml
|
87
|
-
- store_test_results:
|
88
|
-
path: "/tmp/test-results/3.1"
|
89
|
-
- store_artifacts:
|
90
|
-
path: "/tmp/test-results/3.1"
|
91
|
-
workflows:
|
92
|
-
version: 2
|
93
|
-
all_rubies:
|
94
|
-
jobs:
|
95
|
-
- ruby__2_6
|
96
|
-
- ruby__2_7
|
97
|
-
- ruby__3_0
|
98
|
-
- ruby__3_1
|
data/bin/mk_circle_config
DELETED
@@ -1,84 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "yaml"
|
4
|
-
require "pathname"
|
5
|
-
|
6
|
-
circle_config = {
|
7
|
-
"version" => "2.1",
|
8
|
-
"jobs" => {},
|
9
|
-
"workflows" => {
|
10
|
-
"version" => 2,
|
11
|
-
"all_rubies" => {
|
12
|
-
"jobs" => [
|
13
|
-
],
|
14
|
-
},
|
15
|
-
}
|
16
|
-
}
|
17
|
-
|
18
|
-
supported_rubies = [
|
19
|
-
"2.6",
|
20
|
-
"2.7",
|
21
|
-
"3.0",
|
22
|
-
"3.1",
|
23
|
-
]
|
24
|
-
|
25
|
-
supported_rubies.each do |ruby_verison|
|
26
|
-
|
27
|
-
test_results_dir = "/tmp/test-results/#{ruby_verison}"
|
28
|
-
job_name = "ruby__#{ruby_verison.gsub(/\./,"_")}"
|
29
|
-
|
30
|
-
job = {
|
31
|
-
"docker" => [
|
32
|
-
{
|
33
|
-
"image" => "cimg/ruby:#{ruby_verison}",
|
34
|
-
}
|
35
|
-
],
|
36
|
-
"steps" => [
|
37
|
-
"checkout",
|
38
|
-
{
|
39
|
-
"run" => {
|
40
|
-
"name" => "Setup for build",
|
41
|
-
"command" => "bin/setup",
|
42
|
-
}
|
43
|
-
},
|
44
|
-
{
|
45
|
-
"run" => {
|
46
|
-
"name" => "Ensure bin/setup is idempotent",
|
47
|
-
"command" => "bin/setup",
|
48
|
-
}
|
49
|
-
},
|
50
|
-
{
|
51
|
-
"run" => {
|
52
|
-
"name" => "Create the test results dir",
|
53
|
-
"command" => "mkdir -p #{test_results_dir}",
|
54
|
-
}
|
55
|
-
},
|
56
|
-
{
|
57
|
-
"run" => {
|
58
|
-
"name" => "Run all tests",
|
59
|
-
"command" => "bin/ci #{test_results_dir}/rspec_results.xml",
|
60
|
-
}
|
61
|
-
},
|
62
|
-
{
|
63
|
-
"store_test_results" => {
|
64
|
-
"path" => test_results_dir,
|
65
|
-
}
|
66
|
-
},
|
67
|
-
{
|
68
|
-
"store_artifacts" => {
|
69
|
-
"path" => test_results_dir,
|
70
|
-
}
|
71
|
-
},
|
72
|
-
]
|
73
|
-
}
|
74
|
-
circle_config["jobs"][job_name] = job
|
75
|
-
circle_config["workflows"]["all_rubies"]["jobs"] << job_name
|
76
|
-
end
|
77
|
-
|
78
|
-
circle_config_file = (Pathname(__FILE__).dirname / ".." / ".circleci" / "config.yml").expand_path
|
79
|
-
File.open(circle_config_file,"w") do |file|
|
80
|
-
file.puts "# THIS IS GENERATED - DO NOT EDIT"
|
81
|
-
file.puts "# regenerate with bin/mk_circle_config"
|
82
|
-
file.puts "# You are very welcome"
|
83
|
-
file.puts circle_config.to_yaml
|
84
|
-
end
|
data/bin/mk_gem
DELETED
@@ -1,73 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "fileutils"
|
4
|
-
require "pathname"
|
5
|
-
|
6
|
-
include FileUtils
|
7
|
-
|
8
|
-
gem_name = ARGV[0]
|
9
|
-
|
10
|
-
if gem_name.nil?
|
11
|
-
puts "usage: #{$0} gem_name"
|
12
|
-
exit 1
|
13
|
-
end
|
14
|
-
|
15
|
-
root = Pathname(__FILE__).dirname / ".."
|
16
|
-
|
17
|
-
module_name = gem_name.split(/_/).map { |part|
|
18
|
-
part.capitalize
|
19
|
-
}.join
|
20
|
-
|
21
|
-
mkdir_p root / "lib"
|
22
|
-
|
23
|
-
File.open(root / "lib" / "#{gem_name}.rb", "w") do |file|
|
24
|
-
file.puts "module #{module_name}"
|
25
|
-
file.puts "end"
|
26
|
-
end
|
27
|
-
|
28
|
-
mkdir_p root / "lib" / gem_name
|
29
|
-
|
30
|
-
File.open(root / "lib" / gem_name / "version.rb", "w") do |file|
|
31
|
-
file.puts "module #{module_name}"
|
32
|
-
file.puts " VERSION=\"1.0.0\""
|
33
|
-
file.puts "end"
|
34
|
-
end
|
35
|
-
|
36
|
-
gemspec = File.read("rubygem.gemspec")
|
37
|
-
File.open(root / "#{gem_name}.gemspec","w") do |file|
|
38
|
-
file.puts "require_relative \"lib/#{gem_name}/version\""
|
39
|
-
file.puts
|
40
|
-
gemspec.split(/\n/).each do |line|
|
41
|
-
if line =~ /^\s*spec.name/
|
42
|
-
file.puts " spec.name = \"#{gem_name}\""
|
43
|
-
elsif line =~ /^\s*spec.version/
|
44
|
-
file.puts " spec.version = #{module_name}::VERSION"
|
45
|
-
elsif line.include?("«gem_name»")
|
46
|
-
file.puts line.gsub(/«gem_name»/,gem_name)
|
47
|
-
else
|
48
|
-
file.puts line
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
license = File.read(root / "LICENSE.md")
|
54
|
-
File.open(root / "LICENSE.md","w") do |file|
|
55
|
-
license.split(/\n/).each_with_index do |line,index|
|
56
|
-
if index == 0
|
57
|
-
file.puts "[#{gem_name}] Copyright (2021) (David Copeland)(“Licensor”)"
|
58
|
-
else
|
59
|
-
file.puts line
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
readme = File.read(root / "README.md")
|
65
|
-
File.open(root / "README.md","w") do |file|
|
66
|
-
license.split(/\n/).each_with_index do |line,index|
|
67
|
-
if index == 0
|
68
|
-
file.puts "# #{gem_name} - does a thing"
|
69
|
-
else
|
70
|
-
file.puts line
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|