air_test 0.1.2 β 0.1.4
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/.rubocop.yml +279 -0
- data/README.md +0 -6
- data/exe/air_test +5 -3
- data/lib/air_test/configuration.rb +6 -4
- data/lib/air_test/engine.rb +10 -0
- data/lib/air_test/github_client.rb +11 -8
- data/lib/air_test/notion_parser.rb +65 -50
- data/lib/air_test/runner.rb +16 -10
- data/lib/air_test/spec_generator.rb +16 -12
- data/lib/air_test/version.rb +1 -1
- data/lib/air_test.rb +11 -7
- data/lib/tasks/air_test.rake +31 -5
- metadata +48 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2de3a0976d4d5aa40c6178ca21af1d429b059106f0184d797c5a3dbf36c546f1
|
4
|
+
data.tar.gz: 4587d75cecbdabd22e760e6fcfa28b6c1cd9ebb12d2eb156414b11740666e40c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e60e1d94293c77fe912a7017145c3331dec7b18c0aae9ba65c4b71436f662d96f43c46185fc3acdbf4981c023b7949db0cae61bbf0a469753b8d6c11f2178e4
|
7
|
+
data.tar.gz: 79b532e0af0ed9923a571fb5013c13171174eabee415f0a7123a60ebcd4388873259f436b7954cc0518ddba1e3af688a2d0b7c0ad5b6e38859154da3914ae536
|
data/.rubocop.yml
CHANGED
@@ -6,3 +6,282 @@ Style/StringLiterals:
|
|
6
6
|
|
7
7
|
Style/StringLiteralsInInterpolation:
|
8
8
|
EnforcedStyle: double_quotes
|
9
|
+
|
10
|
+
Gemspec/AddRuntimeDependency:
|
11
|
+
Enabled: true
|
12
|
+
Gemspec/AttributeAssignment:
|
13
|
+
Enabled: true
|
14
|
+
Gemspec/DeprecatedAttributeAssignment:
|
15
|
+
Enabled: true
|
16
|
+
Gemspec/DevelopmentDependencies:
|
17
|
+
Enabled: true
|
18
|
+
Gemspec/RequireMFA:
|
19
|
+
Enabled: true
|
20
|
+
Layout/LineContinuationLeadingSpace:
|
21
|
+
Enabled: true
|
22
|
+
Layout/LineContinuationSpacing:
|
23
|
+
Enabled: true
|
24
|
+
Layout/LineEndStringConcatenationIndentation:
|
25
|
+
Enabled: true
|
26
|
+
Layout/SpaceBeforeBrackets:
|
27
|
+
Enabled: true
|
28
|
+
Lint/AmbiguousAssignment:
|
29
|
+
Enabled: true
|
30
|
+
Lint/AmbiguousOperatorPrecedence:
|
31
|
+
Enabled: true
|
32
|
+
Lint/AmbiguousRange:
|
33
|
+
Enabled: true
|
34
|
+
Lint/ArrayLiteralInRegexp:
|
35
|
+
Enabled: true
|
36
|
+
Lint/ConstantOverwrittenInRescue:
|
37
|
+
Enabled: true
|
38
|
+
Lint/ConstantReassignment:
|
39
|
+
Enabled: true
|
40
|
+
Lint/CopDirectiveSyntax:
|
41
|
+
Enabled: true
|
42
|
+
Lint/DeprecatedConstants:
|
43
|
+
Enabled: true
|
44
|
+
Lint/DuplicateBranch:
|
45
|
+
Enabled: true
|
46
|
+
Lint/DuplicateMagicComment:
|
47
|
+
Enabled: true
|
48
|
+
Lint/DuplicateMatchPattern:
|
49
|
+
Enabled: true
|
50
|
+
Lint/DuplicateRegexpCharacterClassElement:
|
51
|
+
Enabled: true
|
52
|
+
Lint/DuplicateSetElement:
|
53
|
+
Enabled: true
|
54
|
+
Lint/EmptyBlock:
|
55
|
+
Enabled: true
|
56
|
+
Lint/EmptyClass:
|
57
|
+
Enabled: true
|
58
|
+
Lint/EmptyInPattern:
|
59
|
+
Enabled: true
|
60
|
+
Lint/HashNewWithKeywordArgumentsAsDefault:
|
61
|
+
Enabled: true
|
62
|
+
Lint/IncompatibleIoSelectWithFiberScheduler:
|
63
|
+
Enabled: true
|
64
|
+
Lint/ItWithoutArgumentsInBlock:
|
65
|
+
Enabled: true
|
66
|
+
Lint/LambdaWithoutLiteralBlock:
|
67
|
+
Enabled: true
|
68
|
+
Lint/LiteralAssignmentInCondition:
|
69
|
+
Enabled: true
|
70
|
+
Lint/MixedCaseRange:
|
71
|
+
Enabled: true
|
72
|
+
Lint/NoReturnInBeginEndBlocks:
|
73
|
+
Enabled: true
|
74
|
+
Lint/NonAtomicFileOperation:
|
75
|
+
Enabled: true
|
76
|
+
Lint/NumberedParameterAssignment:
|
77
|
+
Enabled: true
|
78
|
+
Lint/NumericOperationWithConstantResult:
|
79
|
+
Enabled: true
|
80
|
+
Lint/OrAssignmentToConstant:
|
81
|
+
Enabled: true
|
82
|
+
Lint/RedundantDirGlobSort:
|
83
|
+
Enabled: true
|
84
|
+
Lint/RedundantRegexpQuantifiers:
|
85
|
+
Enabled: true
|
86
|
+
Lint/RedundantTypeConversion:
|
87
|
+
Enabled: true
|
88
|
+
Lint/RefinementImportMethods:
|
89
|
+
Enabled: true
|
90
|
+
Lint/RequireRangeParentheses:
|
91
|
+
Enabled: true
|
92
|
+
Lint/RequireRelativeSelfPath:
|
93
|
+
Enabled: true
|
94
|
+
Lint/SharedMutableDefault:
|
95
|
+
Enabled: true
|
96
|
+
Lint/SuppressedExceptionInNumberConversion:
|
97
|
+
Enabled: true
|
98
|
+
Lint/SymbolConversion:
|
99
|
+
Enabled: true
|
100
|
+
Lint/ToEnumArguments:
|
101
|
+
Enabled: true
|
102
|
+
Lint/TripleQuotes:
|
103
|
+
Enabled: true
|
104
|
+
Lint/UnescapedBracketInRegexp:
|
105
|
+
Enabled: true
|
106
|
+
Lint/UnexpectedBlockArity:
|
107
|
+
Enabled: true
|
108
|
+
Lint/UnmodifiedReduceAccumulator:
|
109
|
+
Enabled: true
|
110
|
+
Lint/UselessConstantScoping:
|
111
|
+
Enabled: true
|
112
|
+
Lint/UselessDefaultValueArgument:
|
113
|
+
Enabled: true
|
114
|
+
Lint/UselessDefined:
|
115
|
+
Enabled: true
|
116
|
+
Lint/UselessNumericOperation:
|
117
|
+
Enabled: true
|
118
|
+
Lint/UselessOr:
|
119
|
+
Enabled: true
|
120
|
+
Lint/UselessRescue:
|
121
|
+
Enabled: true
|
122
|
+
Lint/UselessRuby2Keywords:
|
123
|
+
Enabled: true
|
124
|
+
Metrics/CollectionLiteralLength:
|
125
|
+
Enabled: true
|
126
|
+
Naming/BlockForwarding:
|
127
|
+
Enabled: true
|
128
|
+
Naming/PredicateMethod:
|
129
|
+
Enabled: true
|
130
|
+
Security/CompoundHash:
|
131
|
+
Enabled: true
|
132
|
+
Security/IoMethods:
|
133
|
+
Enabled: true
|
134
|
+
Style/AmbiguousEndlessMethodDefinition:
|
135
|
+
Enabled: true
|
136
|
+
Style/ArgumentsForwarding:
|
137
|
+
Enabled: true
|
138
|
+
Style/ArrayIntersect:
|
139
|
+
Enabled: true
|
140
|
+
Style/BitwisePredicate:
|
141
|
+
Enabled: true
|
142
|
+
Style/CollectionCompact:
|
143
|
+
Enabled: true
|
144
|
+
Style/CollectionQuerying:
|
145
|
+
Enabled: true
|
146
|
+
Style/CombinableDefined:
|
147
|
+
Enabled: true
|
148
|
+
Style/ComparableBetween:
|
149
|
+
Enabled: true
|
150
|
+
Style/ComparableClamp:
|
151
|
+
Enabled: true
|
152
|
+
Style/ConcatArrayLiterals:
|
153
|
+
Enabled: true
|
154
|
+
Style/DataInheritance:
|
155
|
+
Enabled: true
|
156
|
+
Style/DigChain:
|
157
|
+
Enabled: true
|
158
|
+
Style/DirEmpty:
|
159
|
+
Enabled: true
|
160
|
+
Style/DocumentDynamicEvalDefinition:
|
161
|
+
Enabled: true
|
162
|
+
Style/EmptyHeredoc:
|
163
|
+
Enabled: true
|
164
|
+
Style/EmptyStringInsideInterpolation:
|
165
|
+
Enabled: true
|
166
|
+
Style/EndlessMethod:
|
167
|
+
Enabled: true
|
168
|
+
Style/EnvHome:
|
169
|
+
Enabled: true
|
170
|
+
Style/ExactRegexpMatch:
|
171
|
+
Enabled: true
|
172
|
+
Style/FetchEnvVar:
|
173
|
+
Enabled: true
|
174
|
+
Style/FileEmpty:
|
175
|
+
Enabled: true
|
176
|
+
Style/FileNull:
|
177
|
+
Enabled: true
|
178
|
+
Style/FileRead:
|
179
|
+
Enabled: true
|
180
|
+
Style/FileTouch:
|
181
|
+
Enabled: true
|
182
|
+
Style/FileWrite:
|
183
|
+
Enabled: true
|
184
|
+
Style/HashConversion:
|
185
|
+
Enabled: true
|
186
|
+
Style/HashExcept:
|
187
|
+
Enabled: true
|
188
|
+
Style/HashFetchChain:
|
189
|
+
Enabled: true
|
190
|
+
Style/HashSlice:
|
191
|
+
Enabled: true
|
192
|
+
Style/IfWithBooleanLiteralBranches:
|
193
|
+
Enabled: true
|
194
|
+
Style/InPatternThen:
|
195
|
+
Enabled: true
|
196
|
+
Style/ItAssignment:
|
197
|
+
Enabled: true
|
198
|
+
Style/ItBlockParameter:
|
199
|
+
Enabled: true
|
200
|
+
Style/KeywordArgumentsMerging:
|
201
|
+
Enabled: true
|
202
|
+
Style/MagicCommentFormat:
|
203
|
+
Enabled: true
|
204
|
+
Style/MapCompactWithConditionalBlock:
|
205
|
+
Enabled: true
|
206
|
+
Style/MapIntoArray:
|
207
|
+
Enabled: true
|
208
|
+
Style/MapToHash:
|
209
|
+
Enabled: true
|
210
|
+
Style/MapToSet:
|
211
|
+
Enabled: true
|
212
|
+
Style/MinMaxComparison:
|
213
|
+
Enabled: true
|
214
|
+
Style/MultilineInPatternThen:
|
215
|
+
Enabled: true
|
216
|
+
Style/NegatedIfElseCondition:
|
217
|
+
Enabled: true
|
218
|
+
Style/NestedFileDirname:
|
219
|
+
Enabled: true
|
220
|
+
Style/NilLambda:
|
221
|
+
Enabled: true
|
222
|
+
Style/NumberedParameters:
|
223
|
+
Enabled: true
|
224
|
+
Style/NumberedParametersLimit:
|
225
|
+
Enabled: true
|
226
|
+
Style/ObjectThen:
|
227
|
+
Enabled: true
|
228
|
+
Style/OpenStructUse:
|
229
|
+
Enabled: true
|
230
|
+
Style/OperatorMethodCall:
|
231
|
+
Enabled: true
|
232
|
+
Style/QuotedSymbols:
|
233
|
+
Enabled: true
|
234
|
+
Style/RedundantArgument:
|
235
|
+
Enabled: true
|
236
|
+
Style/RedundantArrayConstructor:
|
237
|
+
Enabled: true
|
238
|
+
Style/RedundantArrayFlatten:
|
239
|
+
Enabled: true
|
240
|
+
Style/RedundantConstantBase:
|
241
|
+
Enabled: true
|
242
|
+
Style/RedundantCurrentDirectoryInPath:
|
243
|
+
Enabled: true
|
244
|
+
Style/RedundantDoubleSplatHashBraces:
|
245
|
+
Enabled: true
|
246
|
+
Style/RedundantEach:
|
247
|
+
Enabled: true
|
248
|
+
Style/RedundantFilterChain:
|
249
|
+
Enabled: true
|
250
|
+
Style/RedundantFormat:
|
251
|
+
Enabled: true
|
252
|
+
Style/RedundantHeredocDelimiterQuotes:
|
253
|
+
Enabled: true
|
254
|
+
Style/RedundantInitialize:
|
255
|
+
Enabled: true
|
256
|
+
Style/RedundantInterpolationUnfreeze:
|
257
|
+
Enabled: true
|
258
|
+
Style/RedundantLineContinuation:
|
259
|
+
Enabled: true
|
260
|
+
Style/RedundantRegexpArgument:
|
261
|
+
Enabled: true
|
262
|
+
Style/RedundantRegexpConstructor:
|
263
|
+
Enabled: true
|
264
|
+
Style/RedundantSelfAssignmentBranch:
|
265
|
+
Enabled: true
|
266
|
+
Style/RedundantStringEscape:
|
267
|
+
Enabled: true
|
268
|
+
Style/ReturnNilInPredicateMethodDefinition:
|
269
|
+
Enabled: true
|
270
|
+
Style/SafeNavigationChainLength:
|
271
|
+
Enabled: true
|
272
|
+
Style/SelectByRegexp:
|
273
|
+
Enabled: true
|
274
|
+
Style/SendWithLiteralMethodName:
|
275
|
+
Enabled: true
|
276
|
+
Style/SingleLineDoEndBlock:
|
277
|
+
Enabled: true
|
278
|
+
Style/StringChars:
|
279
|
+
Enabled: true
|
280
|
+
Style/SuperArguments:
|
281
|
+
Enabled: true
|
282
|
+
Style/SuperWithArgsParentheses:
|
283
|
+
Enabled: true
|
284
|
+
Style/SwapValues:
|
285
|
+
Enabled: true
|
286
|
+
Style/YAMLFileRead:
|
287
|
+
Enabled: true
|
data/README.md
CHANGED
@@ -95,12 +95,6 @@ GITHUB_BOT_TOKEN=ghp_xxx
|
|
95
95
|
|
96
96
|
---
|
97
97
|
|
98
|
-
## π§ͺ Tests (optional)
|
99
|
-
|
100
|
-
If you want to test the gemβs services, you can add specs in the `spec/` folder.
|
101
|
-
|
102
|
-
---
|
103
|
-
|
104
98
|
## π Troubleshooting
|
105
99
|
|
106
100
|
- **Notion or GitHub authentication error**: check your tokens.
|
data/exe/air_test
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "fileutils"
|
3
5
|
|
4
6
|
GREEN = "\e[32m"
|
5
7
|
YELLOW = "\e[33m"
|
@@ -25,7 +27,7 @@ else
|
|
25
27
|
puts "#{GREEN}β
Created #{initializer_path}#{RESET}"
|
26
28
|
end
|
27
29
|
|
28
|
-
[
|
30
|
+
["spec/features", "spec/steps"].each do |dir|
|
29
31
|
if Dir.exist?(dir)
|
30
32
|
puts "#{YELLOW}β οΈ #{dir} already exists. Skipping.#{RESET}"
|
31
33
|
else
|
@@ -61,4 +63,4 @@ puts "\n⨠All set! Next steps:"
|
|
61
63
|
puts " 1. Fill in your config/initializers/air_test.rb"
|
62
64
|
puts " 2. Add your tokens to .env or your environment"
|
63
65
|
puts " 3. Run: bundle exec rake air_test:generate_specs_from_notion"
|
64
|
-
puts "\nHappy testing! π"
|
66
|
+
puts "\nHappy testing! π"
|
@@ -1,14 +1,16 @@
|
|
1
|
+
# Handles configuration for AirTest, including API tokens and environment variables.
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
module AirTest
|
5
|
+
# Handles configuration for AirTest, including API tokens and environment variables.
|
4
6
|
class Configuration
|
5
7
|
attr_accessor :notion_token, :notion_database_id, :github_token, :repo
|
6
8
|
|
7
9
|
def initialize
|
8
|
-
@notion_token = ENV
|
9
|
-
@notion_database_id = ENV
|
10
|
-
@github_token = ENV[
|
10
|
+
@notion_token = ENV.fetch("NOTION_TOKEN", nil)
|
11
|
+
@notion_database_id = ENV.fetch("NOTION_DATABASE_ID", nil)
|
12
|
+
@github_token = ENV["GITHUB_BOT_TOKEN"] || ENV.fetch("GITHUB_TOKEN", nil)
|
11
13
|
@repo = nil
|
12
14
|
end
|
13
15
|
end
|
14
|
-
end
|
16
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AirTest
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
# This ensures that the Rake tasks are automatically loaded when the gem is included in a Rails app
|
6
|
+
rake_tasks do
|
7
|
+
load File.expand_path("tasks/air_test.rake", __dir__)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
|
+
require "octokit"
|
3
4
|
|
4
5
|
module AirTest
|
6
|
+
# Handles GitHub API interactions for AirTest, such as commits and pull requests.
|
5
7
|
class GithubClient
|
6
8
|
def initialize(config = AirTest.configuration)
|
7
9
|
@github_token = config.github_token
|
@@ -17,18 +19,18 @@ module AirTest
|
|
17
19
|
end
|
18
20
|
files.each { |f| system("git add -f #{f}") }
|
19
21
|
has_changes = !system("git diff --cached --quiet")
|
20
|
-
if has_changes
|
21
|
-
system("git commit -m '#{commit_message}'")
|
22
|
-
end
|
22
|
+
system("git commit -m '#{commit_message}'") if has_changes
|
23
23
|
system("git push origin #{branch}")
|
24
24
|
has_changes
|
25
25
|
end
|
26
26
|
|
27
|
-
|
27
|
+
# rubocop:disable Metrics/MethodLength
|
28
|
+
def create_pull_request(branch, pr_title, pr_body, assignees: ["Notion"])
|
28
29
|
return unless @client && @repo
|
30
|
+
|
29
31
|
@client.create_pull_request(
|
30
32
|
@repo,
|
31
|
-
|
33
|
+
"main",
|
32
34
|
branch,
|
33
35
|
pr_title,
|
34
36
|
pr_body,
|
@@ -38,6 +40,7 @@ module AirTest
|
|
38
40
|
warn "β Erreur lors de la crΓ©ation de la PR : #{e.message}"
|
39
41
|
nil
|
40
42
|
end
|
43
|
+
# rubocop:enable Metrics/MethodLength
|
41
44
|
|
42
45
|
private
|
43
46
|
|
@@ -47,7 +50,7 @@ module AirTest
|
|
47
50
|
|
48
51
|
def detect_repo_from_git
|
49
52
|
remote_url = `git config --get remote.origin.url`.strip
|
50
|
-
remote_url.split(
|
53
|
+
remote_url.split(%r{[:/]}).last(2).join("/").gsub(/\.git$/, "")
|
51
54
|
end
|
52
55
|
end
|
53
|
-
end
|
56
|
+
end
|
@@ -1,85 +1,95 @@
|
|
1
|
+
# Parses Notion tickets and extracts relevant information for spec generation in AirTest.
|
2
|
+
# rubocop:disable Metrics/ClassLength
|
1
3
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
4
|
+
|
5
|
+
require "net/http"
|
6
|
+
require "json"
|
7
|
+
require "uri"
|
5
8
|
|
6
9
|
module AirTest
|
10
|
+
# Parses Notion tickets and extracts relevant information for spec generation in AirTest.
|
7
11
|
class NotionParser
|
8
12
|
def initialize(config = AirTest.configuration)
|
9
13
|
@database_id = config.notion_database_id
|
10
14
|
@notion_token = config.notion_token
|
11
|
-
@base_url =
|
15
|
+
@base_url = "https://api.notion.com/v1"
|
12
16
|
end
|
13
17
|
|
14
18
|
def fetch_tickets(limit: 5)
|
15
19
|
uri = URI("#{@base_url}/databases/#{@database_id}/query")
|
16
20
|
response = make_api_request(uri, { page_size: 100 })
|
17
|
-
return [] unless response.code ==
|
21
|
+
return [] unless response.code == "200"
|
22
|
+
|
18
23
|
data = JSON.parse(response.body)
|
19
|
-
data[
|
24
|
+
data["results"].first(limit)
|
20
25
|
end
|
21
26
|
|
22
27
|
def parse_ticket_content(page_id)
|
23
28
|
blocks = get_page_content(page_id)
|
24
29
|
return nil unless blocks
|
30
|
+
|
25
31
|
parse_content(blocks)
|
26
32
|
end
|
27
33
|
|
28
34
|
def extract_ticket_title(ticket)
|
29
|
-
ticket.dig(
|
35
|
+
ticket.dig("properties", "Projects", "title", 0, "plain_text") || "No title"
|
30
36
|
end
|
31
37
|
|
32
38
|
def extract_ticket_id(ticket)
|
33
|
-
ticket.dig(
|
39
|
+
ticket.dig("properties", "ID", "unique_id", "number") || "No ID"
|
34
40
|
end
|
35
41
|
|
36
42
|
def extract_ticket_url(ticket)
|
37
|
-
ticket[
|
43
|
+
ticket["url"] || "https://www.notion.so/#{ticket["id"].gsub("-", "")}"
|
38
44
|
end
|
39
45
|
|
40
46
|
private
|
41
47
|
|
42
48
|
def get_page_content(page_id)
|
43
49
|
uri = URI("#{@base_url}/blocks/#{page_id}/children")
|
44
|
-
response = make_api_request(uri, nil,
|
45
|
-
return nil unless response.code ==
|
50
|
+
response = make_api_request(uri, nil, "GET")
|
51
|
+
return nil unless response.code == "200"
|
52
|
+
|
46
53
|
data = JSON.parse(response.body)
|
47
|
-
data[
|
54
|
+
data["results"]
|
48
55
|
end
|
49
56
|
|
50
|
-
|
57
|
+
# rubocop:disable Metrics/MethodLength
|
58
|
+
def make_api_request(uri, request_body = nil, method = "POST")
|
51
59
|
http = Net::HTTP.new(uri.host, uri.port)
|
52
60
|
http.use_ssl = true
|
53
|
-
if method ==
|
61
|
+
if method == "GET"
|
54
62
|
request = Net::HTTP::Get.new(uri)
|
55
63
|
else
|
56
64
|
request = Net::HTTP::Post.new(uri)
|
57
65
|
request.body = request_body.to_json if request_body
|
58
66
|
end
|
59
|
-
request[
|
60
|
-
request[
|
61
|
-
request[
|
67
|
+
request["Authorization"] = "Bearer #{@notion_token}"
|
68
|
+
request["Notion-Version"] = "2022-06-28"
|
69
|
+
request["Content-Type"] = "application/json"
|
62
70
|
http.request(request)
|
63
71
|
end
|
72
|
+
# rubocop:enable Metrics/MethodLength
|
64
73
|
|
74
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
|
65
75
|
def parse_content(blocks)
|
66
76
|
parsed_data = {
|
67
|
-
feature:
|
77
|
+
feature: "",
|
68
78
|
scenarios: [],
|
69
|
-
meta: { tags: [], priority:
|
79
|
+
meta: { tags: [], priority: "", estimate: nil, assignee: "" }
|
70
80
|
}
|
71
81
|
current_scenario = nil
|
72
82
|
in_feature_block = false
|
73
83
|
in_scenario_block = false
|
74
84
|
blocks.each do |block|
|
75
|
-
case block[
|
76
|
-
when
|
77
|
-
heading_text = extract_text(block[block[
|
78
|
-
if heading_text.downcase.include?(
|
85
|
+
case block["type"]
|
86
|
+
when "heading_1", "heading_2", "heading_3"
|
87
|
+
heading_text = extract_text(block[block["type"]]["rich_text"])
|
88
|
+
if heading_text.downcase.include?("feature")
|
79
89
|
in_feature_block = true
|
80
90
|
in_scenario_block = false
|
81
91
|
parsed_data[:feature] = heading_text
|
82
|
-
elsif heading_text.downcase.include?(
|
92
|
+
elsif heading_text.downcase.include?("scenario")
|
83
93
|
in_scenario_block = true
|
84
94
|
in_feature_block = false
|
85
95
|
current_scenario = { title: heading_text, steps: [] }
|
@@ -88,33 +98,36 @@ module AirTest
|
|
88
98
|
in_feature_block = false
|
89
99
|
in_scenario_block = false
|
90
100
|
end
|
91
|
-
when
|
92
|
-
text = extract_text(block[
|
101
|
+
when "paragraph"
|
102
|
+
text = extract_text(block["paragraph"]["rich_text"])
|
93
103
|
next if text.empty?
|
104
|
+
|
94
105
|
if in_feature_block
|
95
|
-
parsed_data[:feature] += "\n"
|
106
|
+
parsed_data[:feature] += "\n#{text}"
|
96
107
|
elsif in_scenario_block && current_scenario
|
97
108
|
current_scenario[:steps] << text
|
98
109
|
end
|
99
|
-
when
|
100
|
-
text = extract_text(block[block[
|
110
|
+
when "bulleted_list_item", "numbered_list_item"
|
111
|
+
text = extract_text(block[block["type"]]["rich_text"])
|
101
112
|
next if text.empty?
|
113
|
+
|
102
114
|
if in_feature_block
|
103
|
-
parsed_data[:feature] += "\nβ’ "
|
115
|
+
parsed_data[:feature] += "\nβ’ #{text}"
|
104
116
|
elsif in_scenario_block && current_scenario
|
105
117
|
current_scenario[:steps] << text
|
106
118
|
end
|
107
|
-
when
|
108
|
-
text = extract_text(block[
|
119
|
+
when "callout"
|
120
|
+
text = extract_text(block["callout"]["rich_text"])
|
109
121
|
next if text.empty?
|
110
|
-
|
122
|
+
|
123
|
+
if text.downcase.include?("tag")
|
111
124
|
tags = extract_tags(text)
|
112
125
|
parsed_data[:meta][:tags].concat(tags)
|
113
|
-
elsif text.downcase.include?(
|
126
|
+
elsif text.downcase.include?("priority")
|
114
127
|
parsed_data[:meta][:priority] = extract_priority(text)
|
115
|
-
elsif text.downcase.include?(
|
128
|
+
elsif text.downcase.include?("estimate")
|
116
129
|
parsed_data[:meta][:estimate] = extract_estimate(text)
|
117
|
-
elsif text.downcase.include?(
|
130
|
+
elsif text.downcase.include?("assignee")
|
118
131
|
parsed_data[:meta][:assignee] = extract_assignee(text)
|
119
132
|
end
|
120
133
|
end
|
@@ -123,16 +136,18 @@ module AirTest
|
|
123
136
|
parsed_data[:meta][:tags] = parsed_data[:meta][:tags].uniq
|
124
137
|
parsed_data
|
125
138
|
end
|
139
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
|
126
140
|
|
127
141
|
def extract_text(rich_text_array)
|
128
|
-
return
|
129
|
-
|
142
|
+
return "" unless rich_text_array
|
143
|
+
|
144
|
+
rich_text_array.map { |text| text["plain_text"] }.join(" ")
|
130
145
|
end
|
131
146
|
|
132
147
|
def extract_tags(text)
|
133
148
|
if text =~ /tags?:\s*(.+)/i
|
134
|
-
tags_text =
|
135
|
-
tags_text.split(/[
|
149
|
+
tags_text = ::Regexp.last_match(1).strip
|
150
|
+
tags_text.split(/[,\s]+/).map(&:strip).reject(&:empty?)
|
136
151
|
else
|
137
152
|
[]
|
138
153
|
end
|
@@ -140,26 +155,26 @@ module AirTest
|
|
140
155
|
|
141
156
|
def extract_priority(text)
|
142
157
|
if text =~ /priority:\s*(.+)/i
|
143
|
-
|
158
|
+
::Regexp.last_match(1).strip
|
144
159
|
else
|
145
|
-
|
160
|
+
""
|
146
161
|
end
|
147
162
|
end
|
148
163
|
|
149
164
|
def extract_estimate(text)
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
nil
|
154
|
-
end
|
165
|
+
return unless text =~ /estimate:\s*(\d+)/i
|
166
|
+
|
167
|
+
::Regexp.last_match(1).to_i
|
155
168
|
end
|
156
169
|
|
157
170
|
def extract_assignee(text)
|
158
171
|
if text =~ /assignee:\s*(.+)/i
|
159
|
-
|
172
|
+
::Regexp.last_match(1).strip
|
160
173
|
else
|
161
|
-
|
174
|
+
""
|
162
175
|
end
|
163
176
|
end
|
164
177
|
end
|
165
|
-
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# rubocop:enable Metrics/ClassLength
|
data/lib/air_test/runner.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Runs the main automation workflow for AirTest, orchestrating Notion parsing and GitHub actions.
|
4
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
|
3
5
|
module AirTest
|
6
|
+
# Runs the main automation workflow for AirTest, orchestrating Notion parsing and GitHub actions.
|
4
7
|
class Runner
|
5
8
|
def initialize(config = AirTest.configuration)
|
6
9
|
@notion = NotionParser.new(config)
|
@@ -16,26 +19,28 @@ module AirTest
|
|
16
19
|
title = @notion.extract_ticket_title(ticket)
|
17
20
|
url = @notion.extract_ticket_url(ticket)
|
18
21
|
puts "\nπ Processing: FDR#{ticket_id} - #{title}"
|
19
|
-
parsed_data = @notion.parse_ticket_content(ticket[
|
22
|
+
parsed_data = @notion.parse_ticket_content(ticket["id"])
|
20
23
|
next unless parsed_data && parsed_data[:feature] && !parsed_data[:feature].empty?
|
21
|
-
|
24
|
+
|
25
|
+
slug = parsed_data[:feature].downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
|
22
26
|
branch = "air_test/#{ticket_id}-#{slug}"
|
23
27
|
spec_path = @spec.generate_spec_from_parsed_data(ticket_id, parsed_data)
|
24
28
|
step_path = @spec.generate_step_definitions_for_spec(spec_path)
|
25
29
|
files_to_commit = [spec_path]
|
26
30
|
files_to_commit << step_path if step_path
|
27
|
-
has_changes = @github.commit_and_push_branch(branch, files_to_commit,
|
31
|
+
has_changes = @github.commit_and_push_branch(branch, files_to_commit,
|
32
|
+
"Add specs for Notion ticket #{ticket_id}")
|
28
33
|
if has_changes
|
29
34
|
pr_title = title
|
30
|
-
scenarios_md = parsed_data[:scenarios].map.with_index(1) do |sc,
|
31
|
-
steps = sc[:steps]&.join(
|
35
|
+
scenarios_md = parsed_data[:scenarios].map.with_index(1) do |sc, _i|
|
36
|
+
steps = sc[:steps]&.join(" ")
|
32
37
|
" - [ ] #{sc[:title]} β #{steps}"
|
33
38
|
end.join("\n")
|
34
39
|
pr_body = <<~MD
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
40
|
+
- **Story Notion :** #{url}
|
41
|
+
- **Feature** : #{parsed_data[:feature]}
|
42
|
+
- **ScΓ©narios** :
|
43
|
+
#{scenarios_md}
|
39
44
|
MD
|
40
45
|
pr = @github.create_pull_request(branch, pr_title, pr_body)
|
41
46
|
puts "β
Pull Request créée : #{pr.html_url}" if pr
|
@@ -45,4 +50,5 @@ module AirTest
|
|
45
50
|
end
|
46
51
|
end
|
47
52
|
end
|
48
|
-
end
|
53
|
+
end
|
54
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
|
@@ -1,14 +1,18 @@
|
|
1
|
+
# Generates RSpec and Turnip spec files from parsed Notion ticket data for AirTest.
|
1
2
|
# frozen_string_literal: true
|
2
|
-
|
3
|
+
|
4
|
+
require "fileutils"
|
3
5
|
|
4
6
|
module AirTest
|
7
|
+
# Generates RSpec and Turnip spec files from parsed Notion ticket data for AirTest.
|
5
8
|
class SpecGenerator
|
6
|
-
|
7
|
-
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
10
|
+
def generate_spec_from_parsed_data(ticket_id, parsed_data, output_dir: "spec/features")
|
11
|
+
feature_slug = parsed_data[:feature].downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
|
8
12
|
filename = "#{feature_slug}_fdr#{ticket_id}.rb"
|
9
13
|
filepath = File.join(output_dir, filename)
|
10
14
|
FileUtils.mkdir_p(output_dir)
|
11
|
-
File.open(filepath,
|
15
|
+
File.open(filepath, "w") do |f|
|
12
16
|
f.puts "# language: turnip"
|
13
17
|
f.puts "# frozen_string_literal: true\n"
|
14
18
|
f.puts "feature '#{parsed_data[:feature]}' do"
|
@@ -24,19 +28,18 @@ module AirTest
|
|
24
28
|
filepath
|
25
29
|
end
|
26
30
|
|
27
|
-
def generate_step_definitions_for_spec(spec_filepath, output_dir:
|
31
|
+
def generate_step_definitions_for_spec(spec_filepath, output_dir: "spec/steps")
|
28
32
|
step_texts = []
|
29
33
|
File.readlines(spec_filepath).each do |line|
|
30
|
-
if line =~ /pending ['\"](.*)['\"]/
|
31
|
-
step_texts << $1.strip
|
32
|
-
end
|
34
|
+
step_texts << ::Regexp.last_match(1).strip if line =~ /pending ['\"](.*)['\"]/
|
33
35
|
end
|
34
36
|
return if step_texts.empty?
|
35
|
-
|
37
|
+
|
38
|
+
spec_filename = File.basename(spec_filepath, ".rb")
|
36
39
|
step_file = File.join(output_dir, "#{spec_filename}_steps.rb")
|
37
40
|
FileUtils.mkdir_p(output_dir)
|
38
|
-
File.open(step_file,
|
39
|
-
f.puts "# Auto-generated step definitions for #{spec_filename.gsub(
|
41
|
+
File.open(step_file, "w") do |f|
|
42
|
+
f.puts "# Auto-generated step definitions for #{spec_filename.gsub("-", " ")}"
|
40
43
|
step_texts.uniq.each do |step|
|
41
44
|
f.puts "\nstep '#{step}' do"
|
42
45
|
f.puts " pending 'Implement: #{step.gsub("'", "\\'")}'"
|
@@ -45,5 +48,6 @@ module AirTest
|
|
45
48
|
end
|
46
49
|
step_file
|
47
50
|
end
|
51
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
48
52
|
end
|
49
|
-
end
|
53
|
+
end
|
data/lib/air_test/version.rb
CHANGED
data/lib/air_test.rb
CHANGED
@@ -1,12 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require_relative "air_test_automation/configuration"
|
5
|
-
require_relative "air_test_automation/notion_parser"
|
6
|
-
require_relative "air_test_automation/spec_generator"
|
7
|
-
require_relative "air_test_automation/github_client"
|
8
|
-
require_relative "air_test_automation/rake_tasks"
|
9
|
-
|
3
|
+
# Main namespace for the AirTest gem, which automates spec generation and GitHub integration.
|
10
4
|
module AirTest
|
11
5
|
class << self
|
12
6
|
attr_accessor :configuration
|
@@ -17,3 +11,13 @@ module AirTest
|
|
17
11
|
yield(configuration)
|
18
12
|
end
|
19
13
|
end
|
14
|
+
|
15
|
+
require_relative "air_test/version"
|
16
|
+
require_relative "air_test/configuration"
|
17
|
+
require_relative "air_test/notion_parser"
|
18
|
+
require_relative "air_test/spec_generator"
|
19
|
+
require_relative "air_test/github_client"
|
20
|
+
require_relative "air_test/runner"
|
21
|
+
|
22
|
+
# Load Rails engine if Rails is available (for Rake task support)
|
23
|
+
require_relative "air_test/engine" if defined?(Rails)
|
data/lib/tasks/air_test.rake
CHANGED
@@ -1,7 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :air_test do
|
2
|
-
desc "Generate specs and PR from Notion"
|
3
|
-
task generate_specs_from_notion: :environment do
|
4
|
-
require
|
5
|
-
|
4
|
+
desc "Generate specs and PR from Notion tickets"
|
5
|
+
task :generate_specs_from_notion, [:limit] => :environment do |_task, args|
|
6
|
+
require "air_test/runner"
|
7
|
+
|
8
|
+
limit = (args[:limit] || 5).to_i
|
9
|
+
puts "π Starting AirTest with limit: #{limit}"
|
10
|
+
|
11
|
+
begin
|
12
|
+
runner = AirTest::Runner.new
|
13
|
+
runner.run(limit: limit)
|
14
|
+
puts "β
AirTest completed successfully!"
|
15
|
+
rescue StandardError => e
|
16
|
+
puts "β AirTest failed: #{e.message}"
|
17
|
+
puts e.backtrace.first(5).join("\n")
|
18
|
+
exit 1
|
19
|
+
end
|
6
20
|
end
|
7
|
-
|
21
|
+
|
22
|
+
desc "Show AirTest configuration"
|
23
|
+
task config: :environment do
|
24
|
+
require "air_test/configuration"
|
25
|
+
|
26
|
+
config = AirTest.configuration
|
27
|
+
puts "π§ AirTest Configuration:"
|
28
|
+
puts " Notion Token: #{config.notion_token ? "Set" : "Not set"}"
|
29
|
+
puts " Notion Database ID: #{config.notion_database_id || "Not set"}"
|
30
|
+
puts " GitHub Token: #{config.github_token ? "Set" : "Not set"}"
|
31
|
+
puts " Repository: #{config.repo || "Not set"}"
|
32
|
+
end
|
33
|
+
end
|
metadata
CHANGED
@@ -1,16 +1,58 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: air_test
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- julien bouland
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-07-
|
11
|
-
dependencies:
|
10
|
+
date: 2025-07-19 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '6.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '6.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: faraday-retry
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: octokit
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '7.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '7.0'
|
12
54
|
description: Automate the generation of Turnip/RSpec specs from Notion tickets, create
|
13
|
-
branches, commits, pushes, and GitHub Pull Requests
|
55
|
+
branches, commits, pushes, and GitHub Pull Requests. All with a single Rake command.
|
14
56
|
email:
|
15
57
|
- bouland.julien@gmail.com
|
16
58
|
executables:
|
@@ -28,6 +70,7 @@ files:
|
|
28
70
|
- exe/air_test
|
29
71
|
- lib/air_test.rb
|
30
72
|
- lib/air_test/configuration.rb
|
73
|
+
- lib/air_test/engine.rb
|
31
74
|
- lib/air_test/github_client.rb
|
32
75
|
- lib/air_test/notion_parser.rb
|
33
76
|
- lib/air_test/runner.rb
|
@@ -42,6 +85,7 @@ metadata:
|
|
42
85
|
homepage_uri: https://github.com/airtest-dev/airtest
|
43
86
|
source_code_uri: https://github.com/airtest-dev/airtest
|
44
87
|
changelog_uri: https://github.com/airtest-io/air_test/blob/main/CHANGELOG.md
|
88
|
+
rubygems_mfa_required: 'true'
|
45
89
|
rdoc_options: []
|
46
90
|
require_paths:
|
47
91
|
- lib
|