modsvaskr 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/modsvaskr/config.rb +41 -4
- data/lib/modsvaskr/game.rb +119 -1
- data/lib/modsvaskr/games/skyrim_se.rb +11 -10
- data/lib/modsvaskr/in_game_tests_runner.rb +335 -0
- data/lib/modsvaskr/logger.rb +21 -2
- data/lib/modsvaskr/run_cmd.rb +2 -2
- data/lib/modsvaskr/tests_runner.rb +47 -276
- data/lib/modsvaskr/tests_suite.rb +0 -0
- data/lib/modsvaskr/tests_suites/exterior_cell.rb +6 -7
- data/lib/modsvaskr/tests_suites/interior_cell.rb +6 -7
- data/lib/modsvaskr/tests_suites/npc.rb +6 -7
- data/lib/modsvaskr/tests_suites/npc_head.rb +51 -0
- data/lib/modsvaskr/ui.rb +40 -6
- data/lib/modsvaskr/version.rb +1 -1
- data/lib/modsvaskr/xedit.rb +0 -0
- data/xedit_scripts/Modsvaskr_DumpInfo.pas +1 -1
- metadata +61 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf22182b9e2f38b08900947b107bac303c8d9c6eebbc7b10ce80b88a899fd4da
|
4
|
+
data.tar.gz: 58a6a9f16573766eec853391ef1aa0efd2a649a691e530a16bbc8de0268c11b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87849b6c50f3cc08db5d150fada9511ff412ab2458310a2f6e64c6126f7037e7e656a379c383b7598449e728be0f925bdb06df39a2e760895c4c9b3a1aca85ff
|
7
|
+
data.tar.gz: 10edfe86882d700c6a052276810b9e21aff2022a89e6b4d082ca03ab4eedb00a8ac0604e93a637492778823c7720133103c72d5adeb06d1240ada02d58a75fb5
|
data/lib/modsvaskr/config.rb
CHANGED
@@ -12,7 +12,19 @@ module Modsvaskr
|
|
12
12
|
# Parameters::
|
13
13
|
# * *file* (String): File containing configuration
|
14
14
|
def initialize(file)
|
15
|
-
@config = YAML.load(File.read(file))
|
15
|
+
@config = YAML.load(File.read(file)) || {}
|
16
|
+
# Parse all game types plugins
|
17
|
+
# Hash<Symbol, Class>
|
18
|
+
@game_types = Hash[
|
19
|
+
Dir.glob("#{__dir__}/games/*.rb").map do |game_type_file|
|
20
|
+
require game_type_file
|
21
|
+
base_name = File.basename(game_type_file, '.rb')
|
22
|
+
[
|
23
|
+
base_name.to_sym,
|
24
|
+
Games.const_get(base_name.split('_').collect(&:capitalize).join.to_sym)
|
25
|
+
]
|
26
|
+
end
|
27
|
+
]
|
16
28
|
end
|
17
29
|
|
18
30
|
# Get the games list
|
@@ -21,9 +33,10 @@ module Modsvaskr
|
|
21
33
|
# * Array<Game>: List of games
|
22
34
|
def games
|
23
35
|
unless defined?(@games)
|
24
|
-
@games = @config['games'].map do |game_info|
|
25
|
-
|
26
|
-
|
36
|
+
@games = (@config['games'] || []).map do |game_info|
|
37
|
+
game_type = game_info['type'].to_sym
|
38
|
+
raise "Unknown game type: #{game_type}. Available ones are #{@game_types.keys.join(', ')}" unless @game_types.key?(game_type)
|
39
|
+
@game_types[game_type].new(self, game_info)
|
27
40
|
end
|
28
41
|
end
|
29
42
|
@games
|
@@ -37,6 +50,30 @@ module Modsvaskr
|
|
37
50
|
@config['xedit']
|
38
51
|
end
|
39
52
|
|
53
|
+
# Return the 7-Zip path
|
54
|
+
#
|
55
|
+
# Result::
|
56
|
+
# * String: The 7-Zip path
|
57
|
+
def seven_zip_path
|
58
|
+
@config['7zip']
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return the automated keys to apply
|
62
|
+
#
|
63
|
+
# Result::
|
64
|
+
# * Array<String>: The list of automated keys
|
65
|
+
def auto_keys
|
66
|
+
@config['auto_keys'] || []
|
67
|
+
end
|
68
|
+
|
69
|
+
# Return the no_prompt flag
|
70
|
+
#
|
71
|
+
# Result::
|
72
|
+
# * Boolean: no_prompt flag
|
73
|
+
def no_prompt
|
74
|
+
@config['no_prompt'] || false
|
75
|
+
end
|
76
|
+
|
40
77
|
end
|
41
78
|
|
42
79
|
end
|
data/lib/modsvaskr/game.rb
CHANGED
@@ -16,10 +16,22 @@ module Modsvaskr
|
|
16
16
|
# * *game_info* (Hash<String,Object>): Game info:
|
17
17
|
# * *name* (String): Game name
|
18
18
|
# * *path* (String): Game installation dir
|
19
|
+
# * *launch_exe* (String): Executable to be launched
|
20
|
+
# * *min_launch_time_secs* (Integer): Minimum expected lauch time for the game, in seconds [default: 10]
|
21
|
+
# * *tests_poll_secs* (Integer): Interval in seconds to be respected between 2 test statuses polling [default: 5]
|
22
|
+
# * *timeout_frozen_tests_secs* (Integer): Timeout in seconds of a frozen game [default: 300]
|
23
|
+
# * *timeout_interrupt_tests_secs* (Integer): Timeout in seconds for the player to interrupt a tests session before restarting the game [default: 10]
|
19
24
|
def initialize(config, game_info)
|
20
25
|
@config = config
|
21
|
-
|
26
|
+
# Set default values here
|
27
|
+
@game_info = {
|
28
|
+
'min_launch_time_secs' => 10,
|
29
|
+
'tests_poll_secs' => 5,
|
30
|
+
'timeout_frozen_tests_secs' => 300,
|
31
|
+
'timeout_interrupt_tests_secs' => 10
|
32
|
+
}.merge(game_info)
|
22
33
|
@name = name
|
34
|
+
@pid = nil
|
23
35
|
init if self.respond_to?(:init)
|
24
36
|
end
|
25
37
|
|
@@ -47,6 +59,30 @@ module Modsvaskr
|
|
47
59
|
@game_info['launch_exe']
|
48
60
|
end
|
49
61
|
|
62
|
+
# Return the tests polling interval
|
63
|
+
#
|
64
|
+
# Result::
|
65
|
+
# * Integer: Tests polling interval
|
66
|
+
def tests_poll_secs
|
67
|
+
@game_info['tests_poll_secs']
|
68
|
+
end
|
69
|
+
|
70
|
+
# Return the timeout to detect a frozen game
|
71
|
+
#
|
72
|
+
# Result::
|
73
|
+
# * Integer: Timeout to detect a frozen game
|
74
|
+
def timeout_frozen_tests_secs
|
75
|
+
@game_info['timeout_frozen_tests_secs']
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return the timeout before restarting a game tests session
|
79
|
+
#
|
80
|
+
# Result::
|
81
|
+
# * Integer: Timeout before restarting a game tests session
|
82
|
+
def timeout_interrupt_tests_secs
|
83
|
+
@game_info['timeout_interrupt_tests_secs']
|
84
|
+
end
|
85
|
+
|
50
86
|
# Return an xEdit instance for this game
|
51
87
|
#
|
52
88
|
# Result::
|
@@ -56,6 +92,88 @@ module Modsvaskr
|
|
56
92
|
@xedit
|
57
93
|
end
|
58
94
|
|
95
|
+
# Launch the game, and wait for launch to be successful
|
96
|
+
#
|
97
|
+
# Parameters::
|
98
|
+
# * *autoload* (Boolean or String): If false, then launch the game using the normal launcher. If String, then use AutoLoad to load a given saved file (or empty to continue latest save) [default: false].
|
99
|
+
def launch(autoload: false)
|
100
|
+
# Launch the game
|
101
|
+
@idx_launch = 0 unless defined?(@idx_launch)
|
102
|
+
if autoload
|
103
|
+
log "[ Game #{name} ] - Launch game (##{@idx_launch}) using AutoLoad #{autoload}..."
|
104
|
+
autoload_file = "#{path}/Data/AutoLoad.cmd"
|
105
|
+
if File.exist?(autoload_file)
|
106
|
+
run_cmd({
|
107
|
+
dir: path,
|
108
|
+
exe: 'Data\AutoLoad.cmd',
|
109
|
+
args: [autoload]
|
110
|
+
})
|
111
|
+
else
|
112
|
+
log "[ Game #{name} ] - Missing file #{autoload_file}. Can't use AutoLoad to load game automatically. Please install the AutoLoad mod."
|
113
|
+
end
|
114
|
+
else
|
115
|
+
log "[ Game #{name} ] - Launch game (##{@idx_launch}) using configured launcher (#{launch_exe})..."
|
116
|
+
run_cmd({
|
117
|
+
dir: path,
|
118
|
+
exe: launch_exe
|
119
|
+
})
|
120
|
+
end
|
121
|
+
@idx_launch += 1
|
122
|
+
# The game launches asynchronously, so just wait a little bit and check for the process existence
|
123
|
+
sleep @game_info['min_launch_time_secs']
|
124
|
+
tasklist_stdout = nil
|
125
|
+
loop do
|
126
|
+
tasklist_stdout = `tasklist | find "#{running_exe}"`.strip
|
127
|
+
break unless tasklist_stdout.empty?
|
128
|
+
log "[ Game #{name} ] - #{running_exe} is not running. Wait for its startup..."
|
129
|
+
sleep 1
|
130
|
+
end
|
131
|
+
@pid = Integer(tasklist_stdout.split(' ')[1])
|
132
|
+
log "[ Game #{name} ] - #{running_exe} has started with PID #{@pid}"
|
133
|
+
end
|
134
|
+
|
135
|
+
# Is the game currently running?
|
136
|
+
#
|
137
|
+
# Result::
|
138
|
+
# * Boolean: Is the game currently running?
|
139
|
+
def running?
|
140
|
+
if @pid
|
141
|
+
running = true
|
142
|
+
begin
|
143
|
+
# Process.kill does not work when the game has crashed (the process is still detected as zombie)
|
144
|
+
# running = Process.kill(0, @pid) == 1
|
145
|
+
tasklist_stdout = `tasklist | find "#{running_exe}"`.strip
|
146
|
+
running = !tasklist_stdout.empty?
|
147
|
+
# log "[ Game #{name} ] - Tasklist returned no #{running_exe}:\n#{tasklist_stdout}" unless running
|
148
|
+
rescue Errno::ESRCH
|
149
|
+
log "[ Game #{name} ] - Got error while waiting for #{running_exe} PID #{@pid}: #{$!}"
|
150
|
+
running = false
|
151
|
+
end
|
152
|
+
@pid = nil unless running
|
153
|
+
running
|
154
|
+
else
|
155
|
+
false
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Kill the game, and wait till it is killed
|
160
|
+
def kill
|
161
|
+
if @pid
|
162
|
+
first_time = true
|
163
|
+
while @pid do
|
164
|
+
system "taskkill #{first_time ? '' : '/F '}/pid #{@pid}"
|
165
|
+
first_time = false
|
166
|
+
sleep 1
|
167
|
+
if running?
|
168
|
+
log "[ Game #{name} ] - #{running_exe} is still running (PID #{@pid}). Wait for its kill..."
|
169
|
+
sleep 5
|
170
|
+
end
|
171
|
+
end
|
172
|
+
else
|
173
|
+
log "[ Game #{name} ] - Game not started, so nothing to kill."
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
59
177
|
end
|
60
178
|
|
61
179
|
end
|
@@ -9,11 +9,6 @@ module Modsvaskr
|
|
9
9
|
# Handle a Skyrim installation
|
10
10
|
class SkyrimSe < Game
|
11
11
|
|
12
|
-
SEVEN_ZIP_CMD = {
|
13
|
-
dir: 'C:\Program Files\7-Zip',
|
14
|
-
exe: '7z.exe'
|
15
|
-
}
|
16
|
-
|
17
12
|
# Initialize the game
|
18
13
|
# [API] - This method is optional
|
19
14
|
def init
|
@@ -28,8 +23,8 @@ module Modsvaskr
|
|
28
23
|
def complete_game_menu(menu)
|
29
24
|
menu.item 'Install SKSE64' do
|
30
25
|
install_skse64
|
31
|
-
|
32
|
-
|
26
|
+
out 'Press Enter to continue...'
|
27
|
+
wait_for_user_enter
|
33
28
|
end
|
34
29
|
end
|
35
30
|
|
@@ -60,7 +55,7 @@ module Modsvaskr
|
|
60
55
|
|
61
56
|
# Install SKSE64 corresponding to our game
|
62
57
|
def install_skse64
|
63
|
-
doc = Nokogiri::HTML(open('https://skse.silverlock.org/'))
|
58
|
+
doc = Nokogiri::HTML(URI.open('https://skse.silverlock.org/'))
|
64
59
|
p_element = doc.css('p').find { |el| el.text.strip =~ /^Current SE build .+: 7z archive$/ }
|
65
60
|
if p_element.nil?
|
66
61
|
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.'
|
@@ -69,14 +64,20 @@ module Modsvaskr
|
|
69
64
|
path = "#{@tmp_dir}/skse64.7z"
|
70
65
|
FileUtils.mkdir_p File.dirname(path)
|
71
66
|
log "Download from #{url} => #{path}..."
|
72
|
-
open(url, 'rb') do |web_io|
|
67
|
+
URI.open(url, 'rb') do |web_io|
|
73
68
|
File.write(path, web_io.read, mode: 'wb')
|
74
69
|
end
|
75
70
|
skse64_tmp_dir = "#{@tmp_dir}/skse64"
|
76
71
|
log "Unzip into #{skse64_tmp_dir}..."
|
77
72
|
FileUtils.rm_rf skse64_tmp_dir
|
78
73
|
FileUtils.mkdir_p skse64_tmp_dir
|
79
|
-
run_cmd
|
74
|
+
run_cmd(
|
75
|
+
{
|
76
|
+
dir: @config.seven_zip_path,
|
77
|
+
exe: '7z.exe'
|
78
|
+
},
|
79
|
+
args: ['x', "\"#{path}\"", "-o\"#{skse64_tmp_dir}\"", '-r']
|
80
|
+
)
|
80
81
|
skse64_subdir = Dir.glob("#{skse64_tmp_dir}/*").first
|
81
82
|
log "Move files from #{skse64_subdir} to #{self.path}..."
|
82
83
|
FileUtils.cp_r "#{skse64_subdir}/.", self.path, remove_destination: true
|
@@ -0,0 +1,335 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'elder_scrolls_plugin'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'json'
|
5
|
+
require 'time'
|
6
|
+
require 'modsvaskr/tests_suite'
|
7
|
+
|
8
|
+
module Modsvaskr
|
9
|
+
|
10
|
+
# Class getting a simple API to handle tests that are run in-game
|
11
|
+
class InGameTestsRunner
|
12
|
+
|
13
|
+
include Logger
|
14
|
+
|
15
|
+
# Constructor.
|
16
|
+
# Default values are for a standard Skyrim SE installation.
|
17
|
+
#
|
18
|
+
# Parameters::
|
19
|
+
# * *config* (Config): Main configuration
|
20
|
+
# * *game* (Game): Game for which we run tests
|
21
|
+
def initialize(config, game)
|
22
|
+
@config = config
|
23
|
+
@game = game
|
24
|
+
auto_test_esp = "#{@game.path}/Data/AutoTest.esp"
|
25
|
+
# Ordered list of available in-game test suites
|
26
|
+
# Array<Symbol>
|
27
|
+
@available_tests_suites =
|
28
|
+
if File.exist?(auto_test_esp)
|
29
|
+
Base64.decode64(
|
30
|
+
ElderScrollsPlugin.new(auto_test_esp).
|
31
|
+
to_json[:sub_chunks].
|
32
|
+
find { |chunk| chunk[:decoded_header][:label] == 'QUST' }[:sub_chunks].
|
33
|
+
find do |chunk|
|
34
|
+
chunk[:sub_chunks].any? { |sub_chunk| sub_chunk[:name] == 'EDID' && sub_chunk[:data] =~ /AutoTest_ScriptsQuest/ }
|
35
|
+
end[:sub_chunks].
|
36
|
+
find { |chunk| chunk[:name] == 'VMAD' }[:data]
|
37
|
+
).scan(/AutoTest_Suite_(\w+)/).flatten.map { |tests_suite| tests_suite.downcase.to_sym }
|
38
|
+
else
|
39
|
+
log "[ In-game testing #{@game.name} ] - Missing file #{auto_test_esp}. In-game tests will be disabled. Please install the AutoTest mod."
|
40
|
+
[]
|
41
|
+
end
|
42
|
+
log "[ In-game testing #{@game.name} ] - #{@available_tests_suites.size} available in-game tests suites: #{@available_tests_suites.join(', ')}"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Run in-game tests in a loop until they are all tested
|
46
|
+
#
|
47
|
+
# Parameters::
|
48
|
+
# * *selected_tests* (Hash<Symbol, Array<String> >): Ordered list of in-game tests to be run, per in-game tests suite
|
49
|
+
# * Proc: Code called when a in-game test status has changed
|
50
|
+
# * Parameters::
|
51
|
+
# * *in_game_tests_suite* (Symbol): The in-game tests suite for which test statuses have changed
|
52
|
+
# * *in_game_tests_statuses* (Hash<String,String>): Tests statuses, per test name
|
53
|
+
def run(selected_tests)
|
54
|
+
unknown_tests_suites = selected_tests.keys - @available_tests_suites
|
55
|
+
log "[ In-game testing #{@game.name} ] - !!! The following in-game tests suites are not supported: #{unknown_tests_suites.join(', ')}" unless unknown_tests_suites.empty?
|
56
|
+
tests_to_run = selected_tests.select { |tests_suite, _tests| !unknown_tests_suites.include?(tests_suite) }
|
57
|
+
unless tests_to_run.empty?
|
58
|
+
FileUtils.mkdir_p "#{@game.path}/Data/SKSE/Plugins/StorageUtilData"
|
59
|
+
tests_to_run.each do |tests_suite, tests|
|
60
|
+
# Write the JSON file that contains the list of tests to run
|
61
|
+
File.write(
|
62
|
+
"#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{tests_suite}_Run.json",
|
63
|
+
JSON.pretty_generate(
|
64
|
+
'stringList' => {
|
65
|
+
'tests_to_run' => tests
|
66
|
+
}
|
67
|
+
)
|
68
|
+
)
|
69
|
+
# Clear the AutoTest test statuses
|
70
|
+
File.unlink("#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{tests_suite}_Statuses.json") if File.exist?("#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{tests_suite}_Statuses.json")
|
71
|
+
end
|
72
|
+
auto_test_config_file = "#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_Config.json"
|
73
|
+
# Write the JSON file that contains the configuration of the AutoTest tests runner
|
74
|
+
File.write(
|
75
|
+
auto_test_config_file,
|
76
|
+
JSON.pretty_generate(
|
77
|
+
'string' => {
|
78
|
+
'on_start' => 'run',
|
79
|
+
'on_stop' => 'exit'
|
80
|
+
}
|
81
|
+
)
|
82
|
+
)
|
83
|
+
out ''
|
84
|
+
out '=========================================='
|
85
|
+
out '= In-game tests are about to be launched ='
|
86
|
+
out '=========================================='
|
87
|
+
out ''
|
88
|
+
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):'
|
89
|
+
out '* Load the game save you want to test (or start a new game).'
|
90
|
+
out ''
|
91
|
+
out 'This will execute all in-game tests automatically.'
|
92
|
+
out ''
|
93
|
+
out 'It is possible that the game crashes during tests:'
|
94
|
+
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.'
|
95
|
+
out '* In case of game crash (CTD), the Modsvaskr test framework will relaunch it automatically and resume testing from when it crashed.'
|
96
|
+
out '* In case of repeated CTD on the same test, the Modsvaskr test framework will detect it and skip the crashing test automatically.'
|
97
|
+
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.'
|
98
|
+
out ''
|
99
|
+
out 'If you want to interrupt in-game testing: invoke the console with ~ key and type stop_tests followed by Enter.'
|
100
|
+
out ''
|
101
|
+
out 'Press enter to start in-game testing (this will lauch your game automatically)...'
|
102
|
+
wait_for_user_enter
|
103
|
+
last_time_tests_changed = nil
|
104
|
+
with_auto_test_monitoring(
|
105
|
+
on_auto_test_statuses_diffs: proc do |in_game_tests_suite, in_game_tests_statuses|
|
106
|
+
yield in_game_tests_suite, in_game_tests_statuses
|
107
|
+
last_time_tests_changed = Time.now
|
108
|
+
end
|
109
|
+
) do
|
110
|
+
# Loop on (re-)launching the game when we still have tests to perform
|
111
|
+
idx_launch = 0
|
112
|
+
loop do
|
113
|
+
# Check which test is supposed to run first, as it will help in knowing if it fails or not.
|
114
|
+
first_tests_suite_to_run = nil
|
115
|
+
first_test_to_run = nil
|
116
|
+
current_tests_statuses = check_auto_test_statuses
|
117
|
+
@available_tests_suites.each do |tests_suite|
|
118
|
+
if tests_to_run.key?(tests_suite)
|
119
|
+
found_test_ok =
|
120
|
+
if current_tests_statuses.key?(tests_suite)
|
121
|
+
# Find the first test that would be run (meaning the first one having no status, or status 'started')
|
122
|
+
tests_to_run[tests_suite].find do |test_name|
|
123
|
+
found_test_name, found_test_status = current_tests_statuses[tests_suite].find { |(current_test_name, _current_test_status)| current_test_name == test_name }
|
124
|
+
found_test_name.nil? || found_test_status == 'started'
|
125
|
+
end
|
126
|
+
else
|
127
|
+
# For sure the first test of this suite will be the first one to run
|
128
|
+
tests_to_run[tests_suite].first
|
129
|
+
end
|
130
|
+
if found_test_ok
|
131
|
+
first_tests_suite_to_run = tests_suite
|
132
|
+
first_test_to_run = found_test_ok
|
133
|
+
break
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
if first_tests_suite_to_run.nil?
|
138
|
+
log "[ In-game testing #{@game.name} ] - No more test to be run."
|
139
|
+
break
|
140
|
+
else
|
141
|
+
log "[ In-game testing #{@game.name} ] - First test to run should be #{first_tests_suite_to_run} / #{first_test_to_run}."
|
142
|
+
# Launch the game to execute AutoTest
|
143
|
+
@game.launch(autoload: idx_launch == 0 ? false : 'auto_test')
|
144
|
+
idx_launch += 1
|
145
|
+
log "[ In-game testing #{@game.name} ] - Start monitoring in-game testing..."
|
146
|
+
last_time_tests_changed = Time.now
|
147
|
+
while @game.running? do
|
148
|
+
check_auto_test_statuses
|
149
|
+
# If the tests haven't changed for too long, consider the game has frozen, but not crashed. So kill it.
|
150
|
+
if Time.now - last_time_tests_changed > @game.timeout_frozen_tests_secs
|
151
|
+
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."
|
152
|
+
@game.kill
|
153
|
+
else
|
154
|
+
sleep @game.tests_poll_secs
|
155
|
+
end
|
156
|
+
end
|
157
|
+
last_test_statuses = check_auto_test_statuses
|
158
|
+
# Log latest statuses
|
159
|
+
log "[ In-game testing #{@game.name} ] - End monitoring in-game testing. In-game test statuses after game run:"
|
160
|
+
last_test_statuses.each do |tests_suite, statuses_for_type|
|
161
|
+
log "[ In-game testing #{@game.name} ] - [ #{tests_suite} ] - #{statuses_for_type.select { |(_name, status)| status == 'ok' }.size} / #{statuses_for_type.size}"
|
162
|
+
end
|
163
|
+
# Check for which reason the game has stopped, and eventually end the testing session.
|
164
|
+
# Careful as this JSON file can be written by Papyrus that treat strings as case insensitive.
|
165
|
+
# cf. https://github.com/xanderdunn/skaar/wiki/Common-Tasks
|
166
|
+
auto_test_config = Hash[JSON.parse(File.read(auto_test_config_file))['string'].map { |key, value| [key.downcase, value.downcase] }]
|
167
|
+
if auto_test_config.dig('stopped_by') == 'user'
|
168
|
+
log "[ In-game testing #{@game.name} ] - Tests have been stopped by user."
|
169
|
+
break
|
170
|
+
end
|
171
|
+
if auto_test_config.dig('tests_execution') == 'end'
|
172
|
+
log "[ In-game testing #{@game.name} ] - Tests have finished running."
|
173
|
+
break
|
174
|
+
end
|
175
|
+
# From here we know that the game has either crashed or has been killed.
|
176
|
+
# This is an abnormal termination of the game.
|
177
|
+
# We have to know if this is due to a specific test that fails deterministically, or if it is the engine being unstable.
|
178
|
+
# Check the status of the first test that should have been run to know about it.
|
179
|
+
first_test_status = nil
|
180
|
+
_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)
|
181
|
+
if first_test_status == 'ok'
|
182
|
+
# It's not necessarily deterministic.
|
183
|
+
# We just have to go on executing next tests.
|
184
|
+
log "[ In-game testing #{@game.name} ] - Tests session has finished in error, certainly due to the game's normal instability. Will resume testing."
|
185
|
+
else
|
186
|
+
# The first test doesn't pass.
|
187
|
+
# We need to mark it as failed, then remove it from the runs.
|
188
|
+
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."
|
189
|
+
# 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.
|
190
|
+
if first_test_status == 'started' || first_test_status == '' || first_test_status.nil?
|
191
|
+
File.write(
|
192
|
+
"#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{first_tests_suite_to_run}_Statuses.json",
|
193
|
+
JSON.pretty_generate(
|
194
|
+
'string' => Hash[((last_test_statuses[first_tests_suite_to_run] || []) + [[first_test_to_run, '']]).map do |(test_name, test_status)|
|
195
|
+
[
|
196
|
+
test_name,
|
197
|
+
test_name == first_test_to_run ? 'failed_ctd' : test_status
|
198
|
+
]
|
199
|
+
end]
|
200
|
+
)
|
201
|
+
)
|
202
|
+
# Notify the callbacks updating test statuses
|
203
|
+
check_auto_test_statuses
|
204
|
+
end
|
205
|
+
end
|
206
|
+
# We will start again. Leave some time to interrupt if we want.
|
207
|
+
out "We are going to start again in #{@game.timeout_interrupt_tests_secs} seconds. Press Enter now to interrupt it."
|
208
|
+
key_pressed =
|
209
|
+
begin
|
210
|
+
Timeout.timeout(@game.timeout_interrupt_tests_secs) { $stdin.gets }
|
211
|
+
rescue
|
212
|
+
nil
|
213
|
+
end
|
214
|
+
if key_pressed
|
215
|
+
log "[ In-game testing #{@game.name} ] - Run interrupted by user."
|
216
|
+
# 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.
|
217
|
+
break
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
# Start an AutoTest monitoring session.
|
228
|
+
# This allows checking for test statuses differences easily.
|
229
|
+
#
|
230
|
+
# Parameters::
|
231
|
+
# * *on_auto_test_statuses_diffs* (Proc): Code called when a in-game test status has changed
|
232
|
+
# * Parameters::
|
233
|
+
# * *in_game_tests_suite* (Symbol): The in-game tests suite for which test statuses have changed
|
234
|
+
# * *in_game_tests_statuses* (Hash<String,String>): Tests statuses, per test name
|
235
|
+
# * Proc: Code called with monitoring on
|
236
|
+
def with_auto_test_monitoring(on_auto_test_statuses_diffs:)
|
237
|
+
@last_auto_test_statuses = {}
|
238
|
+
@on_auto_test_statuses_diffs = on_auto_test_statuses_diffs
|
239
|
+
yield
|
240
|
+
end
|
241
|
+
|
242
|
+
# Check AutoTest test statuses differences, and call some code in case of changes in those statuses.
|
243
|
+
# Remember the last checked statuses to only find diffs on next call.
|
244
|
+
# Prerequisites: To be called inside a with_auto_test_monitoring block
|
245
|
+
#
|
246
|
+
# Result::
|
247
|
+
# * Hash<Symbol, Array<[String, String]> >: Ordered list of AutoTest [test name, test status], per AutoTest tests suite
|
248
|
+
def check_auto_test_statuses
|
249
|
+
# Log a diff in tests
|
250
|
+
new_statuses = auto_test_statuses
|
251
|
+
diff_statuses = diff_statuses(@last_auto_test_statuses, new_statuses)
|
252
|
+
unless diff_statuses.empty?
|
253
|
+
# Tests have progressed
|
254
|
+
log "[ In-game testing #{@game.name} ] - #{diff_statuses.size} tests suites have statuses changes:"
|
255
|
+
diff_statuses.each do |tests_suite, tests_statuses|
|
256
|
+
log "[ In-game testing #{@game.name} ] * #{tests_suite}:"
|
257
|
+
tests_statuses.each do |(test_name, test_status)|
|
258
|
+
log "[ In-game testing #{@game.name} ] * #{test_name}: #{test_status}"
|
259
|
+
end
|
260
|
+
@on_auto_test_statuses_diffs.call(tests_suite, Hash[tests_statuses])
|
261
|
+
end
|
262
|
+
end
|
263
|
+
# Remember the current statuses
|
264
|
+
@last_auto_test_statuses = new_statuses
|
265
|
+
@last_auto_test_statuses
|
266
|
+
end
|
267
|
+
|
268
|
+
# Get the list of AutoTest statuses
|
269
|
+
#
|
270
|
+
# Result::
|
271
|
+
# * Hash<Symbol, Array<[String, String]> >: Ordered list of AutoTest [test name, test status], per AutoTest tests suite
|
272
|
+
def auto_test_statuses
|
273
|
+
statuses = {}
|
274
|
+
`dir "#{@game.path}/Data/SKSE/Plugins/StorageUtilData" /B`.split("\n").each do |file|
|
275
|
+
if file =~ /^AutoTest_(.+)_Statuses\.json$/
|
276
|
+
auto_test_suite = $1.downcase.to_sym
|
277
|
+
# Careful as this JSON file can be written by Papyrus that treat strings as case insensitive.
|
278
|
+
# cf. https://github.com/xanderdunn/skaar/wiki/Common-Tasks
|
279
|
+
statuses[auto_test_suite] = JSON.parse(File.read("#{@game.path}/Data/SKSE/Plugins/StorageUtilData/#{file}"))['string'].map do |test_name, test_status|
|
280
|
+
[test_name.downcase, test_status.downcase]
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
statuses
|
285
|
+
end
|
286
|
+
|
287
|
+
# Diff between test statuses.
|
288
|
+
#
|
289
|
+
# Parameters::
|
290
|
+
# * *statuses1* (Hash<Symbol, Array<[String, String]> >): Test statuses, per test name (in a sorted list), per tests suite
|
291
|
+
# * *statuses2* (Hash<Symbol, Array<[String, String]> >): Test statuses, per test name (in a sorted list), per tests suite
|
292
|
+
# Result::
|
293
|
+
# * Hash<Symbol, Array<[String, String]> >: statuses2 - statuses1
|
294
|
+
def diff_statuses(statuses1, statuses2)
|
295
|
+
statuses = {}
|
296
|
+
statuses1.each do |tests_suite, tests_info|
|
297
|
+
if statuses2.key?(tests_suite)
|
298
|
+
# Handle Hashes as it will be faster
|
299
|
+
statuses1_for_test = Hash[tests_info]
|
300
|
+
statuses2_for_test = Hash[statuses2[tests_suite]]
|
301
|
+
statuses1_for_test.each do |test_name, status1|
|
302
|
+
if statuses2_for_test.key?(test_name)
|
303
|
+
if statuses2_for_test[test_name] != status1
|
304
|
+
# Change in status
|
305
|
+
statuses[tests_suite] = [] unless statuses.key?(tests_suite)
|
306
|
+
statuses[tests_suite] << [test_name, statuses2_for_test[test_name]]
|
307
|
+
end
|
308
|
+
else
|
309
|
+
# This test has been removed
|
310
|
+
statuses[tests_suite] = [] unless statuses.key?(tests_suite)
|
311
|
+
statuses[tests_suite] << [test_name, 'deleted']
|
312
|
+
end
|
313
|
+
end
|
314
|
+
statuses2_for_test.each do |test_name, status2|
|
315
|
+
unless statuses1_for_test.key?(test_name)
|
316
|
+
# This test has been added
|
317
|
+
statuses[tests_suite] = [] unless statuses.key?(tests_suite)
|
318
|
+
statuses[tests_suite] << [test_name, status2]
|
319
|
+
end
|
320
|
+
end
|
321
|
+
else
|
322
|
+
# All test statuses have been removed
|
323
|
+
statuses[tests_suite] = tests_info.map { |(test_name, _test_status)| [test_name, 'deleted'] }
|
324
|
+
end
|
325
|
+
end
|
326
|
+
statuses2.each do |tests_suite, tests_info|
|
327
|
+
# All test statuses have been added
|
328
|
+
statuses[tests_suite] = tests_info unless statuses1.key?(tests_suite)
|
329
|
+
end
|
330
|
+
statuses
|
331
|
+
end
|
332
|
+
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|