modsvaskr 0.1.8 → 0.1.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/modsvaskr +11 -11
- data/lib/modsvaskr/config.rb +78 -79
- data/lib/modsvaskr/encoding.rb +31 -0
- data/lib/modsvaskr/game.rb +208 -179
- data/lib/modsvaskr/games/skyrim_se.rb +106 -92
- data/lib/modsvaskr/in_game_tests_runner.rb +348 -348
- data/lib/modsvaskr/logger.rb +45 -44
- data/lib/modsvaskr/run_cmd.rb +23 -22
- data/lib/modsvaskr/tests_runner.rb +204 -205
- data/lib/modsvaskr/tests_suite.rb +70 -69
- data/lib/modsvaskr/tests_suites/exterior_cell.rb +120 -117
- data/lib/modsvaskr/tests_suites/interior_cell.rb +63 -63
- data/lib/modsvaskr/tests_suites/npc.rb +67 -39
- data/lib/modsvaskr/tests_suites/npc_head.rb +6 -10
- data/lib/modsvaskr/ui.rb +205 -205
- data/lib/modsvaskr/version.rb +5 -5
- data/lib/modsvaskr/xedit.rb +65 -52
- data/xedit_scripts/Modsvaskr_DumpInfo.pas +13 -0
- metadata +39 -10
@@ -1,92 +1,106 @@
|
|
1
|
-
require 'open-uri'
|
2
|
-
require 'tmpdir'
|
3
|
-
require 'nokogiri'
|
4
|
-
|
5
|
-
module Modsvaskr
|
6
|
-
|
7
|
-
module Games
|
8
|
-
|
9
|
-
# Handle a Skyrim installation
|
10
|
-
class SkyrimSe < Game
|
11
|
-
|
12
|
-
# Initialize the game
|
13
|
-
# [API] - This method is optional
|
14
|
-
def init
|
15
|
-
@tmp_dir = "#{Dir.tmpdir}/modsvaskr"
|
16
|
-
end
|
17
|
-
|
18
|
-
# Complete the game menu
|
19
|
-
# [API] - This method is optional
|
20
|
-
#
|
21
|
-
# Parameters::
|
22
|
-
# * *menu* (CursesMenu): Menu to complete
|
23
|
-
def complete_game_menu(menu)
|
24
|
-
menu.item 'Install SKSE64' do
|
25
|
-
install_skse64
|
26
|
-
out 'Press Enter to continue...'
|
27
|
-
wait_for_user_enter
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
# Get the game running executable name (that can be found in a tasks manager)
|
32
|
-
# [API] - This method is mandatory
|
33
|
-
#
|
34
|
-
# Result::
|
35
|
-
# * String: The running exe name
|
36
|
-
def running_exe
|
37
|
-
'SkyrimSE.exe'
|
38
|
-
end
|
39
|
-
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
1
|
+
require 'open-uri'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module Modsvaskr
|
6
|
+
|
7
|
+
module Games
|
8
|
+
|
9
|
+
# Handle a Skyrim installation
|
10
|
+
class SkyrimSe < Game
|
11
|
+
|
12
|
+
# Initialize the game
|
13
|
+
# [API] - This method is optional
|
14
|
+
def init
|
15
|
+
@tmp_dir = "#{Dir.tmpdir}/modsvaskr"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Complete the game menu
|
19
|
+
# [API] - This method is optional
|
20
|
+
#
|
21
|
+
# Parameters::
|
22
|
+
# * *menu* (CursesMenu): Menu to complete
|
23
|
+
def complete_game_menu(menu)
|
24
|
+
menu.item 'Install SKSE64' do
|
25
|
+
install_skse64
|
26
|
+
out 'Press Enter to continue...'
|
27
|
+
wait_for_user_enter
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the game running executable name (that can be found in a tasks manager)
|
32
|
+
# [API] - This method is mandatory
|
33
|
+
#
|
34
|
+
# Result::
|
35
|
+
# * String: The running exe name
|
36
|
+
def running_exe
|
37
|
+
'SkyrimSE.exe'
|
38
|
+
end
|
39
|
+
|
40
|
+
# Ordered list of default esps present in the game (the ones in the Data folder when 0 mod is being used).
|
41
|
+
# The list is ordered according to the game's load order.
|
42
|
+
# [API] - This method is mandatory
|
43
|
+
#
|
44
|
+
# Result::
|
45
|
+
# * Array<String>: List of esp/esm/esl base file names.
|
46
|
+
def game_esps
|
47
|
+
%w[
|
48
|
+
skyrim.esm
|
49
|
+
update.esm
|
50
|
+
dawnguard.esm
|
51
|
+
hearthfires.esm
|
52
|
+
dragonborn.esm
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Read the load order.
|
57
|
+
# [API] - This method is mandatory
|
58
|
+
#
|
59
|
+
# Result::
|
60
|
+
# * Array<String>: List of all active plugins, including masters
|
61
|
+
def read_load_order
|
62
|
+
game_esps +
|
63
|
+
File.read("#{ENV['USERPROFILE']}/AppData/Local/Skyrim Special Edition/plugins.txt").split("\n").map do |line|
|
64
|
+
line =~ /^\*(.+)$/ ? Regexp.last_match(1).downcase : nil
|
65
|
+
end.compact
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Install SKSE64 corresponding to our game
|
71
|
+
def install_skse64
|
72
|
+
doc = Nokogiri::HTML(URI.open('https://skse.silverlock.org/'))
|
73
|
+
p_element = doc.css('p').find { |el| el.text.strip =~ /^Current SE build .+: 7z archive$/ }
|
74
|
+
if p_element.nil?
|
75
|
+
log '!!! Can\'t get SKSE64 from https://skse.silverlock.org/. It looks like the page structure has changed. Please update the code or install it manually.'
|
76
|
+
else
|
77
|
+
url = "https://skse.silverlock.org/#{p_element.at('a')['href']}"
|
78
|
+
path = "#{@tmp_dir}/skse64.7z"
|
79
|
+
FileUtils.mkdir_p File.dirname(path)
|
80
|
+
log "Download from #{url} => #{path}..."
|
81
|
+
URI.parse(url).open('rb') do |web_io|
|
82
|
+
File.write(path, web_io.read, mode: 'wb')
|
83
|
+
end
|
84
|
+
skse64_tmp_dir = "#{@tmp_dir}/skse64"
|
85
|
+
log "Unzip into #{skse64_tmp_dir}..."
|
86
|
+
FileUtils.rm_rf skse64_tmp_dir
|
87
|
+
FileUtils.mkdir_p skse64_tmp_dir
|
88
|
+
run_cmd(
|
89
|
+
{
|
90
|
+
dir: @config.seven_zip_path,
|
91
|
+
exe: '7z.exe'
|
92
|
+
},
|
93
|
+
args: ['x', "\"#{path}\"", "-o\"#{skse64_tmp_dir}\"", '-r']
|
94
|
+
)
|
95
|
+
skse64_subdir = Dir.glob("#{skse64_tmp_dir}/*").first
|
96
|
+
log "Move files from #{skse64_subdir} to #{self.path}..."
|
97
|
+
FileUtils.cp_r "#{skse64_subdir}/.", self.path, remove_destination: true
|
98
|
+
log 'SKSE64 installed successfully.'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -1,348 +1,348 @@
|
|
1
|
-
require 'base64'
|
2
|
-
require 'elder_scrolls_plugin'
|
3
|
-
require 'fileutils'
|
4
|
-
require 'json'
|
5
|
-
require 'time'
|
6
|
-
require 'timeout'
|
7
|
-
require 'modsvaskr/tests_suite'
|
8
|
-
|
9
|
-
module Modsvaskr
|
10
|
-
|
11
|
-
# Class getting a simple API to handle tests that are run in-game
|
12
|
-
class InGameTestsRunner
|
13
|
-
|
14
|
-
include Logger
|
15
|
-
|
16
|
-
# Constructor.
|
17
|
-
# Default values are for a standard Skyrim SE installation.
|
18
|
-
#
|
19
|
-
# Parameters::
|
20
|
-
# * *config* (Config): Main configuration
|
21
|
-
# * *game* (Game): Game for which we run tests
|
22
|
-
def initialize(config, game)
|
23
|
-
@config = config
|
24
|
-
@game = game
|
25
|
-
auto_test_esp = "#{@game.path}/Data/AutoTest.esp"
|
26
|
-
# Ordered list of available in-game test suites
|
27
|
-
# Array<Symbol>
|
28
|
-
@available_tests_suites =
|
29
|
-
if File.exist?(auto_test_esp)
|
30
|
-
Base64.decode64(
|
31
|
-
ElderScrollsPlugin.new(auto_test_esp).
|
32
|
-
to_json[:sub_chunks].
|
33
|
-
find { |chunk| chunk[:decoded_header][:label] == 'QUST' }[:sub_chunks].
|
34
|
-
find do |chunk|
|
35
|
-
chunk[:sub_chunks].any? { |sub_chunk| sub_chunk[:name] == 'EDID' && sub_chunk[:data] =~ /AutoTest_ScriptsQuest/ }
|
36
|
-
end[:sub_chunks].
|
37
|
-
find { |chunk| chunk[:name] == 'VMAD' }[:data]
|
38
|
-
).scan(/AutoTest_Suite_(\w+)/).flatten.map { |tests_suite| tests_suite.downcase.to_sym }
|
39
|
-
else
|
40
|
-
log "[ In-game testing #{@game.name} ] - Missing file #{auto_test_esp}. In-game tests will be disabled. Please install the AutoTest mod."
|
41
|
-
[]
|
42
|
-
end
|
43
|
-
log "[ In-game testing #{@game.name} ] - #{@available_tests_suites.size} available in-game tests suites: #{@available_tests_suites.join(', ')}"
|
44
|
-
end
|
45
|
-
|
46
|
-
# Run in-game tests in a loop until they are all tested
|
47
|
-
#
|
48
|
-
# Parameters::
|
49
|
-
# * *selected_tests* (Hash<Symbol, Array<String> >): Ordered list of in-game tests to be run, per in-game tests suite
|
50
|
-
# * Proc: Code called when a in-game test status has changed
|
51
|
-
# * Parameters::
|
52
|
-
# * *in_game_tests_suite* (Symbol): The in-game tests suite for which test statuses have changed
|
53
|
-
# * *in_game_tests_statuses* (Hash<String,String>): Tests statuses, per test name
|
54
|
-
def run(selected_tests)
|
55
|
-
unknown_tests_suites = selected_tests.keys - @available_tests_suites
|
56
|
-
log "[ In-game testing #{@game.name} ] - !!! The following in-game tests suites are not supported: #{unknown_tests_suites.join(', ')}" unless unknown_tests_suites.empty?
|
57
|
-
tests_to_run = selected_tests.
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
)
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
)
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
end
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
)
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
end
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
234
|
-
end
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
private
|
239
|
-
|
240
|
-
# Start an AutoTest monitoring session.
|
241
|
-
# This allows checking for test statuses differences easily.
|
242
|
-
#
|
243
|
-
# Parameters::
|
244
|
-
# * *on_auto_test_statuses_diffs* (Proc): Code called when a in-game test status has changed
|
245
|
-
# * Parameters::
|
246
|
-
# * *in_game_tests_suite* (Symbol): The in-game tests suite for which test statuses have changed
|
247
|
-
# * *in_game_tests_statuses* (Hash<String,String>): Tests statuses, per test name
|
248
|
-
# * Proc: Code called with monitoring on
|
249
|
-
def with_auto_test_monitoring(on_auto_test_statuses_diffs:)
|
250
|
-
@last_auto_test_statuses = {}
|
251
|
-
@on_auto_test_statuses_diffs = on_auto_test_statuses_diffs
|
252
|
-
yield
|
253
|
-
end
|
254
|
-
|
255
|
-
# Check AutoTest test statuses differences, and call some code in case of changes in those statuses.
|
256
|
-
# Remember the last checked statuses to only find diffs on next call.
|
257
|
-
# Prerequisites: To be called inside a with_auto_test_monitoring block
|
258
|
-
#
|
259
|
-
# Result::
|
260
|
-
# * Hash<Symbol, Array<[String, String]> >: Ordered list of AutoTest [test name, test status], per AutoTest tests suite
|
261
|
-
def check_auto_test_statuses
|
262
|
-
# Log a diff in tests
|
263
|
-
new_statuses = auto_test_statuses
|
264
|
-
diff_statuses = diff_statuses(@last_auto_test_statuses, new_statuses)
|
265
|
-
unless diff_statuses.empty?
|
266
|
-
# Tests have progressed
|
267
|
-
log "[ In-game testing #{@game.name} ] - #{diff_statuses.size} tests suites have statuses changes:"
|
268
|
-
diff_statuses.each do |tests_suite, tests_statuses|
|
269
|
-
log "[ In-game testing #{@game.name} ] * #{tests_suite}:"
|
270
|
-
tests_statuses.each do |(test_name, test_status)|
|
271
|
-
log "[ In-game testing #{@game.name} ] * #{test_name}: #{test_status}"
|
272
|
-
end
|
273
|
-
@on_auto_test_statuses_diffs.call(tests_suite,
|
274
|
-
end
|
275
|
-
end
|
276
|
-
# Remember the current statuses
|
277
|
-
@last_auto_test_statuses = new_statuses
|
278
|
-
@last_auto_test_statuses
|
279
|
-
end
|
280
|
-
|
281
|
-
# Get the list of AutoTest statuses
|
282
|
-
#
|
283
|
-
# Result::
|
284
|
-
# * Hash<Symbol, Array<[String, String]> >: Ordered list of AutoTest [test name, test status], per AutoTest tests suite
|
285
|
-
def auto_test_statuses
|
286
|
-
statuses = {}
|
287
|
-
`dir "#{@game.path}/Data/SKSE/Plugins/StorageUtilData" /B`.split("\n").each do |file|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
end
|
296
|
-
end
|
297
|
-
statuses
|
298
|
-
end
|
299
|
-
|
300
|
-
# Diff between test statuses.
|
301
|
-
#
|
302
|
-
# Parameters::
|
303
|
-
# * *
|
304
|
-
# * *
|
305
|
-
# Result::
|
306
|
-
# * Hash<Symbol, Array<[String, String]> >:
|
307
|
-
def diff_statuses(
|
308
|
-
statuses = {}
|
309
|
-
|
310
|
-
if
|
311
|
-
# Handle Hashes as it will be faster
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
if
|
316
|
-
if
|
317
|
-
# Change in status
|
318
|
-
statuses[tests_suite] = [] unless statuses.key?(tests_suite)
|
319
|
-
statuses[tests_suite] << [test_name,
|
320
|
-
end
|
321
|
-
else
|
322
|
-
# This test has been removed
|
323
|
-
statuses[tests_suite] = [] unless statuses.key?(tests_suite)
|
324
|
-
statuses[tests_suite] << [test_name, 'deleted']
|
325
|
-
end
|
326
|
-
end
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
end
|
334
|
-
else
|
335
|
-
# All test statuses have been removed
|
336
|
-
statuses[tests_suite] = tests_info.map { |(test_name, _test_status)| [test_name, 'deleted'] }
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
# All test statuses have been added
|
341
|
-
statuses[tests_suite] = tests_info unless
|
342
|
-
end
|
343
|
-
statuses
|
344
|
-
end
|
345
|
-
|
346
|
-
end
|
347
|
-
|
348
|
-
end
|
1
|
+
require 'base64'
|
2
|
+
require 'elder_scrolls_plugin'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'json'
|
5
|
+
require 'time'
|
6
|
+
require 'timeout'
|
7
|
+
require 'modsvaskr/tests_suite'
|
8
|
+
|
9
|
+
module Modsvaskr
|
10
|
+
|
11
|
+
# Class getting a simple API to handle tests that are run in-game
|
12
|
+
class InGameTestsRunner
|
13
|
+
|
14
|
+
include Logger
|
15
|
+
|
16
|
+
# Constructor.
|
17
|
+
# Default values are for a standard Skyrim SE installation.
|
18
|
+
#
|
19
|
+
# Parameters::
|
20
|
+
# * *config* (Config): Main configuration
|
21
|
+
# * *game* (Game): Game for which we run tests
|
22
|
+
def initialize(config, game)
|
23
|
+
@config = config
|
24
|
+
@game = game
|
25
|
+
auto_test_esp = "#{@game.path}/Data/AutoTest.esp"
|
26
|
+
# Ordered list of available in-game test suites
|
27
|
+
# Array<Symbol>
|
28
|
+
@available_tests_suites =
|
29
|
+
if File.exist?(auto_test_esp)
|
30
|
+
Base64.decode64(
|
31
|
+
ElderScrollsPlugin.new(auto_test_esp).
|
32
|
+
to_json[:sub_chunks].
|
33
|
+
find { |chunk| chunk[:decoded_header][:label] == 'QUST' }[:sub_chunks].
|
34
|
+
find do |chunk|
|
35
|
+
chunk[:sub_chunks].any? { |sub_chunk| sub_chunk[:name] == 'EDID' && sub_chunk[:data] =~ /AutoTest_ScriptsQuest/ }
|
36
|
+
end[:sub_chunks].
|
37
|
+
find { |chunk| chunk[:name] == 'VMAD' }[:data]
|
38
|
+
).scan(/AutoTest_Suite_(\w+)/).flatten.map { |tests_suite| tests_suite.downcase.to_sym }
|
39
|
+
else
|
40
|
+
log "[ In-game testing #{@game.name} ] - Missing file #{auto_test_esp}. In-game tests will be disabled. Please install the AutoTest mod."
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
log "[ In-game testing #{@game.name} ] - #{@available_tests_suites.size} available in-game tests suites: #{@available_tests_suites.join(', ')}"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Run in-game tests in a loop until they are all tested
|
47
|
+
#
|
48
|
+
# Parameters::
|
49
|
+
# * *selected_tests* (Hash<Symbol, Array<String> >): Ordered list of in-game tests to be run, per in-game tests suite
|
50
|
+
# * Proc: Code called when a in-game test status has changed
|
51
|
+
# * Parameters::
|
52
|
+
# * *in_game_tests_suite* (Symbol): The in-game tests suite for which test statuses have changed
|
53
|
+
# * *in_game_tests_statuses* (Hash<String,String>): Tests statuses, per test name
|
54
|
+
def run(selected_tests)
|
55
|
+
unknown_tests_suites = selected_tests.keys - @available_tests_suites
|
56
|
+
log "[ In-game testing #{@game.name} ] - !!! The following in-game tests suites are not supported: #{unknown_tests_suites.join(', ')}" unless unknown_tests_suites.empty?
|
57
|
+
tests_to_run = selected_tests.reject { |tests_suite, _tests| unknown_tests_suites.include?(tests_suite) }
|
58
|
+
return if tests_to_run.empty?
|
59
|
+
|
60
|
+
FileUtils.mkdir_p "#{@game.path}/Data/SKSE/Plugins/StorageUtilData"
|
61
|
+
tests_to_run.each do |tests_suite, tests|
|
62
|
+
# Write the JSON file that contains the list of tests to run
|
63
|
+
File.write(
|
64
|
+
"#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{tests_suite}_Run.json",
|
65
|
+
JSON.pretty_generate(
|
66
|
+
'stringList' => {
|
67
|
+
'tests_to_run' => tests
|
68
|
+
}
|
69
|
+
)
|
70
|
+
)
|
71
|
+
# Clear the AutoTest test statuses that we are going to run
|
72
|
+
statuses_file = "#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{tests_suite}_Statuses.json"
|
73
|
+
next unless File.exist?(statuses_file)
|
74
|
+
|
75
|
+
File.write(
|
76
|
+
statuses_file,
|
77
|
+
JSON.pretty_generate('string' => JSON.parse(File.read(statuses_file))['string'].delete_if { |test_name, _test_status| tests.include?(test_name) })
|
78
|
+
)
|
79
|
+
end
|
80
|
+
auto_test_config_file = "#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_Config.json"
|
81
|
+
# Write the JSON file that contains the configuration of the AutoTest tests runner
|
82
|
+
File.write(
|
83
|
+
auto_test_config_file,
|
84
|
+
JSON.pretty_generate(
|
85
|
+
'string' => {
|
86
|
+
'on_start' => 'run',
|
87
|
+
'on_stop' => 'exit'
|
88
|
+
}
|
89
|
+
)
|
90
|
+
)
|
91
|
+
out ''
|
92
|
+
out '=========================================='
|
93
|
+
out '= In-game tests are about to be launched ='
|
94
|
+
out '=========================================='
|
95
|
+
out ''
|
96
|
+
out 'Here is what you need to do once the game will be launched (don\'t launch it by yourself, the test framework will launch it for you):'
|
97
|
+
out '* Load the game save you want to test (or start a new game).'
|
98
|
+
out ''
|
99
|
+
out 'This will execute all in-game tests automatically.'
|
100
|
+
out ''
|
101
|
+
out 'It is possible that the game crashes during tests:'
|
102
|
+
out '* That\'s a normal situation, as tests don\'t mimick a realistic gaming experience, and the Bethesda engine is not meant to be stressed like that.'
|
103
|
+
out '* In case of game crash (CTD), the Modsvaskr test framework will relaunch it automatically and resume testing from when it crashed.'
|
104
|
+
out '* In case of repeated CTD on the same test, the Modsvaskr test framework will detect it and skip the crashing test automatically.'
|
105
|
+
out '* In case of a game freeze without CTD, the Modsvaskr test framework will detect it after a few minutes and automatically kill the game before re-launching it to resume testing.'
|
106
|
+
out ''
|
107
|
+
out 'If you want to interrupt in-game testing: invoke the console with ~ key and type stop_tests followed by Enter.'
|
108
|
+
out ''
|
109
|
+
out 'Press enter to start in-game testing (this will lauch your game automatically)...'
|
110
|
+
wait_for_user_enter
|
111
|
+
last_time_tests_changed = nil
|
112
|
+
with_auto_test_monitoring(
|
113
|
+
on_auto_test_statuses_diffs: proc do |in_game_tests_suite, in_game_tests_statuses|
|
114
|
+
yield in_game_tests_suite, in_game_tests_statuses
|
115
|
+
last_time_tests_changed = Time.now
|
116
|
+
end
|
117
|
+
) do
|
118
|
+
# Loop on (re-)launching the game when we still have tests to perform
|
119
|
+
idx_launch = 0
|
120
|
+
loop do
|
121
|
+
# Check which test is supposed to run first, as it will help in knowing if it fails or not.
|
122
|
+
first_tests_suite_to_run = nil
|
123
|
+
first_test_to_run = nil
|
124
|
+
current_tests_statuses = check_auto_test_statuses
|
125
|
+
@available_tests_suites.each do |tests_suite|
|
126
|
+
next unless tests_to_run.key?(tests_suite)
|
127
|
+
|
128
|
+
found_test_ok =
|
129
|
+
if current_tests_statuses.key?(tests_suite)
|
130
|
+
# Find the first test that would be run (meaning the first one having no status, or status 'started')
|
131
|
+
tests_to_run[tests_suite].find do |test_name|
|
132
|
+
found_test_name, found_test_status = current_tests_statuses[tests_suite].find { |(current_test_name, _current_test_status)| current_test_name == test_name }
|
133
|
+
found_test_name.nil? || found_test_status == 'started'
|
134
|
+
end
|
135
|
+
else
|
136
|
+
# For sure the first test of this suite will be the first one to run
|
137
|
+
tests_to_run[tests_suite].first
|
138
|
+
end
|
139
|
+
next unless found_test_ok
|
140
|
+
|
141
|
+
first_tests_suite_to_run = tests_suite
|
142
|
+
first_test_to_run = found_test_ok
|
143
|
+
break
|
144
|
+
end
|
145
|
+
if first_tests_suite_to_run.nil?
|
146
|
+
log "[ In-game testing #{@game.name} ] - No more test to be run."
|
147
|
+
break
|
148
|
+
else
|
149
|
+
log "[ In-game testing #{@game.name} ] - First test to run should be #{first_tests_suite_to_run} / #{first_test_to_run}."
|
150
|
+
# Launch the game to execute AutoTest
|
151
|
+
@game.launch(autoload: idx_launch.zero? ? false : 'auto_test')
|
152
|
+
idx_launch += 1
|
153
|
+
log "[ In-game testing #{@game.name} ] - Start monitoring in-game testing..."
|
154
|
+
last_time_tests_changed = Time.now
|
155
|
+
while @game.running?
|
156
|
+
check_auto_test_statuses
|
157
|
+
# If the tests haven't changed for too long, consider the game has frozen, but not crashed. So kill it.
|
158
|
+
if Time.now - last_time_tests_changed > @game.timeout_frozen_tests_secs
|
159
|
+
log "[ In-game testing #{@game.name} ] - Last time in-game tests statuses have changed is #{last_time_tests_changed.strftime('%F %T')}. Consider the game is frozen, so kill it."
|
160
|
+
@game.kill
|
161
|
+
else
|
162
|
+
sleep @game.tests_poll_secs
|
163
|
+
end
|
164
|
+
end
|
165
|
+
last_test_statuses = check_auto_test_statuses
|
166
|
+
# Log latest statuses
|
167
|
+
log "[ In-game testing #{@game.name} ] - End monitoring in-game testing. In-game test statuses after game run:"
|
168
|
+
last_test_statuses.each do |tests_suite, statuses_for_type|
|
169
|
+
log "[ In-game testing #{@game.name} ] - [ #{tests_suite} ] - #{statuses_for_type.select { |(_name, status)| status == 'ok' }.size} / #{statuses_for_type.size}"
|
170
|
+
end
|
171
|
+
# Check for which reason the game has stopped, and eventually end the testing session.
|
172
|
+
# Careful as this JSON file can be written by Papyrus that treat strings as case insensitive.
|
173
|
+
# cf. https://github.com/xanderdunn/skaar/wiki/Common-Tasks
|
174
|
+
auto_test_config = JSON.parse(File.read(auto_test_config_file))['string'].map { |key, value| [key.downcase, value.downcase] }.to_h
|
175
|
+
if auto_test_config['stopped_by'] == 'user'
|
176
|
+
log "[ In-game testing #{@game.name} ] - Tests have been stopped by user."
|
177
|
+
break
|
178
|
+
end
|
179
|
+
if auto_test_config['tests_execution'] == 'end'
|
180
|
+
log "[ In-game testing #{@game.name} ] - Tests have finished running."
|
181
|
+
break
|
182
|
+
end
|
183
|
+
# From here we know that the game has either crashed or has been killed.
|
184
|
+
# This is an abnormal termination of the game.
|
185
|
+
# We have to know if this is due to a specific test that fails deterministically, or if it is the engine being unstable.
|
186
|
+
# Check the status of the first test that should have been run to know about it.
|
187
|
+
first_test_status = nil
|
188
|
+
_found_test_name, first_test_status = last_test_statuses[first_tests_suite_to_run].find { |(current_test_name, _current_test_status)| current_test_name == first_test_to_run } if last_test_statuses.key?(first_tests_suite_to_run)
|
189
|
+
if first_test_status == 'ok'
|
190
|
+
# It's not necessarily deterministic.
|
191
|
+
# We just have to go on executing next tests.
|
192
|
+
log "[ In-game testing #{@game.name} ] - Tests session has finished in error, certainly due to the game's normal instability. Will resume testing."
|
193
|
+
else
|
194
|
+
# The first test doesn't pass.
|
195
|
+
# We need to mark it as failed, then remove it from the runs.
|
196
|
+
log "[ In-game testing #{@game.name} ] - First test #{first_tests_suite_to_run} / #{first_test_to_run} is in error status: #{first_test_status}. Consider it failed and skip it for next run."
|
197
|
+
# If the test was started but failed before setting its status to something else then change the test status in the JSON file directly so that AutoTest does not try to re-run it.
|
198
|
+
if first_test_status == 'started' || first_test_status == '' || first_test_status.nil?
|
199
|
+
File.write(
|
200
|
+
"#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{first_tests_suite_to_run}_Statuses.json",
|
201
|
+
JSON.pretty_generate(
|
202
|
+
'string' => ((last_test_statuses[first_tests_suite_to_run] || []) + [[first_test_to_run, '']]).map do |(test_name, test_status)|
|
203
|
+
[
|
204
|
+
test_name,
|
205
|
+
test_name == first_test_to_run ? 'failed_ctd' : test_status
|
206
|
+
]
|
207
|
+
end.to_h
|
208
|
+
)
|
209
|
+
)
|
210
|
+
# Notify the callbacks updating test statuses
|
211
|
+
check_auto_test_statuses
|
212
|
+
end
|
213
|
+
end
|
214
|
+
# We will start again. Leave some time to interrupt if we want.
|
215
|
+
if @config.no_prompt
|
216
|
+
out 'Start again automatically as no_prompt has been set.'
|
217
|
+
else
|
218
|
+
# First, flush stdin of any pending character
|
219
|
+
$stdin.getc until select([$stdin], nil, nil, 2).nil?
|
220
|
+
out "We are going to start again in #{@game.timeout_interrupt_tests_secs} seconds. Press Enter now to interrupt it."
|
221
|
+
key_pressed =
|
222
|
+
begin
|
223
|
+
Timeout.timeout(@game.timeout_interrupt_tests_secs) { $stdin.gets }
|
224
|
+
rescue Timeout::Error
|
225
|
+
nil
|
226
|
+
end
|
227
|
+
if key_pressed
|
228
|
+
log "[ In-game testing #{@game.name} ] - Run interrupted by user."
|
229
|
+
# TODO: Remove AutoTest start on load: it has been interrupted by the user, so we should not keep it in case the user launches the game by itself.
|
230
|
+
break
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
# Start an AutoTest monitoring session.
|
241
|
+
# This allows checking for test statuses differences easily.
|
242
|
+
#
|
243
|
+
# Parameters::
|
244
|
+
# * *on_auto_test_statuses_diffs* (Proc): Code called when a in-game test status has changed
|
245
|
+
# * Parameters::
|
246
|
+
# * *in_game_tests_suite* (Symbol): The in-game tests suite for which test statuses have changed
|
247
|
+
# * *in_game_tests_statuses* (Hash<String,String>): Tests statuses, per test name
|
248
|
+
# * Proc: Code called with monitoring on
|
249
|
+
def with_auto_test_monitoring(on_auto_test_statuses_diffs:)
|
250
|
+
@last_auto_test_statuses = {}
|
251
|
+
@on_auto_test_statuses_diffs = on_auto_test_statuses_diffs
|
252
|
+
yield
|
253
|
+
end
|
254
|
+
|
255
|
+
# Check AutoTest test statuses differences, and call some code in case of changes in those statuses.
|
256
|
+
# Remember the last checked statuses to only find diffs on next call.
|
257
|
+
# Prerequisites: To be called inside a with_auto_test_monitoring block
|
258
|
+
#
|
259
|
+
# Result::
|
260
|
+
# * Hash<Symbol, Array<[String, String]> >: Ordered list of AutoTest [test name, test status], per AutoTest tests suite
|
261
|
+
def check_auto_test_statuses
|
262
|
+
# Log a diff in tests
|
263
|
+
new_statuses = auto_test_statuses
|
264
|
+
diff_statuses = diff_statuses(@last_auto_test_statuses, new_statuses)
|
265
|
+
unless diff_statuses.empty?
|
266
|
+
# Tests have progressed
|
267
|
+
log "[ In-game testing #{@game.name} ] - #{diff_statuses.size} tests suites have statuses changes:"
|
268
|
+
diff_statuses.each do |tests_suite, tests_statuses|
|
269
|
+
log "[ In-game testing #{@game.name} ] * #{tests_suite}:"
|
270
|
+
tests_statuses.each do |(test_name, test_status)|
|
271
|
+
log "[ In-game testing #{@game.name} ] * #{test_name}: #{test_status}"
|
272
|
+
end
|
273
|
+
@on_auto_test_statuses_diffs.call(tests_suite, tests_statuses.to_h)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
# Remember the current statuses
|
277
|
+
@last_auto_test_statuses = new_statuses
|
278
|
+
@last_auto_test_statuses
|
279
|
+
end
|
280
|
+
|
281
|
+
# Get the list of AutoTest statuses
|
282
|
+
#
|
283
|
+
# Result::
|
284
|
+
# * Hash<Symbol, Array<[String, String]> >: Ordered list of AutoTest [test name, test status], per AutoTest tests suite
|
285
|
+
def auto_test_statuses
|
286
|
+
statuses = {}
|
287
|
+
`dir "#{@game.path}/Data/SKSE/Plugins/StorageUtilData" /B`.split("\n").each do |file|
|
288
|
+
next unless file =~ /^AutoTest_(.+)_Statuses\.json$/
|
289
|
+
|
290
|
+
auto_test_suite = Regexp.last_match(1).downcase.to_sym
|
291
|
+
# Careful as this JSON file can be written by Papyrus that treat strings as case insensitive.
|
292
|
+
# cf. https://github.com/xanderdunn/skaar/wiki/Common-Tasks
|
293
|
+
statuses[auto_test_suite] = JSON.parse(File.read("#{@game.path}/Data/SKSE/Plugins/StorageUtilData/#{file}"))['string'].map do |test_name, test_status|
|
294
|
+
[test_name.downcase, test_status.downcase]
|
295
|
+
end
|
296
|
+
end
|
297
|
+
statuses
|
298
|
+
end
|
299
|
+
|
300
|
+
# Diff between test statuses.
|
301
|
+
#
|
302
|
+
# Parameters::
|
303
|
+
# * *statuses_1* (Hash<Symbol, Array<[String, String]> >): Test statuses, per test name (in a sorted list), per tests suite
|
304
|
+
# * *statuses_2* (Hash<Symbol, Array<[String, String]> >): Test statuses, per test name (in a sorted list), per tests suite
|
305
|
+
# Result::
|
306
|
+
# * Hash<Symbol, Array<[String, String]> >: statuses_2 - statuses_1
|
307
|
+
def diff_statuses(statuses_1, statuses_2)
|
308
|
+
statuses = {}
|
309
|
+
statuses_1.each do |tests_suite, tests_info|
|
310
|
+
if statuses_2.key?(tests_suite)
|
311
|
+
# Handle Hashes as it will be faster
|
312
|
+
statuses_1_for_test = tests_info.to_h
|
313
|
+
statuses_2_for_test = (statuses_2[tests_suite]).to_h
|
314
|
+
statuses_1_for_test.each do |test_name, status_1|
|
315
|
+
if statuses_2_for_test.key?(test_name)
|
316
|
+
if statuses_2_for_test[test_name] != status_1
|
317
|
+
# Change in status
|
318
|
+
statuses[tests_suite] = [] unless statuses.key?(tests_suite)
|
319
|
+
statuses[tests_suite] << [test_name, statuses_2_for_test[test_name]]
|
320
|
+
end
|
321
|
+
else
|
322
|
+
# This test has been removed
|
323
|
+
statuses[tests_suite] = [] unless statuses.key?(tests_suite)
|
324
|
+
statuses[tests_suite] << [test_name, 'deleted']
|
325
|
+
end
|
326
|
+
end
|
327
|
+
statuses_2_for_test.each do |test_name, status_2|
|
328
|
+
next if statuses_1_for_test.key?(test_name)
|
329
|
+
|
330
|
+
# This test has been added
|
331
|
+
statuses[tests_suite] = [] unless statuses.key?(tests_suite)
|
332
|
+
statuses[tests_suite] << [test_name, status_2]
|
333
|
+
end
|
334
|
+
else
|
335
|
+
# All test statuses have been removed
|
336
|
+
statuses[tests_suite] = tests_info.map { |(test_name, _test_status)| [test_name, 'deleted'] }
|
337
|
+
end
|
338
|
+
end
|
339
|
+
statuses_2.each do |tests_suite, tests_info|
|
340
|
+
# All test statuses have been added
|
341
|
+
statuses[tests_suite] = tests_info unless statuses_1.key?(tests_suite)
|
342
|
+
end
|
343
|
+
statuses
|
344
|
+
end
|
345
|
+
|
346
|
+
end
|
347
|
+
|
348
|
+
end
|