with_clues 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|