Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* AutoStory Failure Capture
*
* From: https://github.com/PokemonAutomation/
*
*/

#include <utility>
#include "Common/Cpp/Color.h"
#include "Common/Cpp/Exceptions.h"
#include "Common/Cpp/Filesystem/Filesystem.h"
#include "Common/Cpp/Json/JsonArray.h"
#include "Common/Cpp/Json/JsonValue.h"
#include "Common/Cpp/Logging/GlobalLogger.h"
#include "Common/Cpp/Logging/LastLogTracker.h"
#include "Common/Cpp/PrettyPrint.h"
#include "CommonFramework/GlobalAutoPaths.h"
#include "CommonFramework/ImageTypes/ImageRGB32.h"
#include "CommonFramework/Recording/StreamHistorySession.h"
#include "CommonFramework/VideoPipeline/VideoFeed.h"
#include "Pokemon_AutoStoryFailureCapture.h"

namespace PokemonAutomation{
namespace Pokemon{


namespace{

// Log lines kept in Failure.json.
constexpr size_t LOG_LINES = 30;

bool folder_safe(char c){
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|| c == '.' || c == '-' || c == '_';
}

std::string folder_safe(std::string_view text){
std::string safe(text);
for (char& c : safe){
if (!folder_safe(c)){
c = '_';
}
}
return safe;
}

std::vector<std::string> recent_log(){
std::vector<std::string> lines;
for (const LogLine& line : global_last_log_history().get_recent(LOG_LINES)){
lines.push_back(line.text);
}
return lines;
}

} // namespace


std::string failure_folder_name(std::string_view time, const FailureRecord& record){
const std::string_view event = record.event.empty() ? std::string_view("checkpoint") : std::string_view(record.event);
return folder_safe(time) + "-" + folder_safe(record.checkpoint) + "-" + folder_safe(event);
}

JsonObject failure_json(const FailureRecord& record){
JsonObject json;
json["game"] = record.game;
json["checkpoint"] = record.checkpoint;
json["event"] = record.event;
json["error"] = record.error;
JsonArray log;
for (const std::string& line : record.log){
log.push_back(line);
}
json["log"] = std::move(log);
return json;
}

StatusStep step_of_status(std::string_view line){
const size_t space = line.find(' ');
const size_t slash = line.find('/');
if (space == std::string_view::npos || slash == std::string_view::npos || slash > space){
return {};
}
const size_t colon = line.find(':', space + 1);
if (colon == std::string_view::npos){
return {};
}
return StatusStep{
std::string(line.substr(0, slash)),
std::string(line.substr(space + 1, colon - space - 1)),
};
}


FailureCapture::FailureCapture(VideoStream& stream, std::string game)
: m_stream(stream)
, m_game(std::move(game))
{}

void FailureCapture::attach(SegmentHooks& hooks){
hooks.status = [this, status = std::move(hooks.status)](const std::string& line) mutable{
StatusStep step = step_of_status(line);
if (!step.checkpoint.empty()){
m_last_step = std::move(step);
}
if (status){
status(line);
}
};
hooks.failed = [this](const std::string& checkpoint, const std::string& event, const Outcome& failure){
save(checkpoint, event, to_string(failure));
};
}

void FailureCapture::save_thrown(const std::string& error){
const std::string checkpoint = m_last_step.checkpoint.empty() ? "unknown" : m_last_step.checkpoint;
save(checkpoint, m_last_step.event, error);
}

std::string FailureCapture::save(const std::string& checkpoint, const std::string& event, const std::string& error){

Check warning on line 118 in SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStoryFailureCapture.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::Pokemon::FailureCapture::save( const std :: string & checkpoint , const std :: string & event , const std :: string & error): function_lines=27 (advisory above 25, limit 50)
try{
const VideoSnapshot snapshot = m_stream.video().snapshot();
FailureRecord record{m_game, checkpoint, event, error, recent_log()};
const std::string folder = USER_FILE_PATH() + m_game + "/AutoStoryFailures/"
+ failure_folder_name(now_to_filestring(), record) + "/";
Filesystem::create_directories(Filesystem::Path(folder));
if (!Filesystem::is_directory(Filesystem::Path(folder))){
m_stream.log("AutoStory failure capture: cannot make " + folder, COLOR_RED);
return "";
}
if (!snapshot || !snapshot->save(folder + "Screenshot.png")){
m_stream.log("AutoStory failure capture: no screenshot.", COLOR_ORANGE);
}
JsonValue(failure_json(record)).dump(folder + "Failure.json");
if (!m_stream.save_stream_history(folder + "Video.mp4")){
m_stream.log("AutoStory failure capture: no video (is Stream History on?).", COLOR_ORANGE);
}
m_stream.log("AutoStory failure capture: " + folder, COLOR_BLUE);
return folder;
}catch (Exception& e){
m_stream.log("AutoStory failure capture failed: " + e.message(), COLOR_RED);
}catch (std::exception& e){
m_stream.log(std::string("AutoStory failure capture failed: ") + e.what(), COLOR_RED);
}
return "";
}



} // namespace Pokemon
} // namespace PokemonAutomation
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* AutoStory Failure Capture
*
* From: https://github.com/PokemonAutomation/
*
* What a failed AutoStory event leaves behind, whatever the error-report
* settings say (a story failure throws with NO_ERROR_REPORT, so no error
* report is written for it): the stream history as a video, the frame on
* screen and a short JSON, in
*
* USER_FILE_PATH()/<game>/AutoStoryFailures/<time>-<checkpoint>-<event>/
* Video.mp4 the stream history, when its setting is on
* Screenshot.png the frame on screen when the event failed
* Failure.json game, checkpoint, event, error, the last log lines
*
* The video holds the whole stream history (the "Stream History" setting's
* seconds), saved before the reload. Pokemon_FrameReplay.h says how to cut
* the seconds before the failure into a frame-replay fixture.
*
*/

#ifndef PokemonAutomation_Pokemon_AutoStoryFailureCapture_H
#define PokemonAutomation_Pokemon_AutoStoryFailureCapture_H

#include <string>
#include <string_view>
#include <vector>
#include "Common/Cpp/Json/JsonObject.h"
#include "CommonFramework/Tools/VideoStream.h"
#include "Pokemon_StorySegments.h"

namespace PokemonAutomation{
namespace Pokemon{


// One failure, as Failure.json holds it.
struct FailureRecord{
std::string game;
std::string checkpoint;
std::string event;
std::string error;
// The last log lines before the failure, oldest first.
std::vector<std::string> log;
};

// "<time>-<checkpoint>-<event>", each character a folder name can't hold
// made "_". An empty event (a start or end check failed) is "checkpoint".
std::string failure_folder_name(std::string_view time, const FailureRecord& record);

// Failure.json: the record's fields, its log lines as an array.
JsonObject failure_json(const FailureRecord& record);

// The step a story status line names.
struct StatusStep{
std::string checkpoint;
std::string event;
};
// "00.03/2 below_oaks_trigger: start" names 00.03 and below_oaks_trigger.
// Both empty for a line that names no event ("00: start, 3 checkpoints").
StatusStep step_of_status(std::string_view line);


// Saves a capture for each failed event of the run it is attached to.
class FailureCapture{
public:
// `game` names the folder under USER_FILE_PATH(): "PokemonBDSP".
FailureCapture(VideoStream& stream, std::string game);

// Sets `hooks.failed`, and wraps `hooks.status` to remember the step
// under way for save_thrown(). Must outlive the hooks.
void attach(SegmentHooks& hooks);

// For an exception out of the run, saved under the last step a status
// line named.
void save_thrown(const std::string& error);

// Writes the folder and returns its path, or "" if it couldn't be
// made. Logs what it skipped; never throws.
std::string save(const std::string& checkpoint, const std::string& event, const std::string& error);

private:
VideoStream& m_stream;
std::string m_game;
StatusStep m_last_step;
};



} // namespace Pokemon
} // namespace PokemonAutomation
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/* AutoStory Failure Capture Tests
*
* From: https://github.com/PokemonAutomation/
*
* The pure parts of the failure capture: its folder names, its JSON and
* the step a status line names. Saving needs a console's stream.
*
*/

#include <string>
#include <utility>
#include "Common/Cpp/Json/JsonArray.h"
#include "Common/Cpp/Json/JsonValue.h"
#include "Common/Cpp/TestRunners/UnitTest.h"
#include "Pokemon_AutoStoryFailureCapture.h"
#include "Pokemon_AutoStory_Tests.h"

namespace PokemonAutomation{
namespace Pokemon{


namespace{

FailureRecord oaks_lab_failure(){
return FailureRecord{
"PokemonFRLG", "00.27", "walk_to_oak", "expect_failed: dialogue opened",
{"[Event] 00.27/3 walk_to_oak: start", "[Event] 00.27/3 walk_to_oak: failed"},
};
}

UnitTestResult test_folder_name(){
FailureRecord record = oaks_lab_failure();
std::string name = failure_folder_name("20261109-110102123456", record);
if (name != "20261109-110102123456-00.27-walk_to_oak"){
return "Unexpected folder name: " + name;
}
record.event = "";
name = failure_folder_name("t", record);
if (name != "t-00.27-checkpoint"){
return "A failed check should be named \"checkpoint\": " + name;
}
record.checkpoint = "00/27";
record.event = "talk: Oak?";
name = failure_folder_name("t", record);
if (name != "t-00_27-talk__Oak_"){
return "Characters a folder can't hold should be \"_\": " + name;
}
return true;
}

UnitTestResult test_json(){
const JsonObject json = failure_json(oaks_lab_failure());
if (json.get_string_throw("game") != "PokemonFRLG"
|| json.get_string_throw("checkpoint") != "00.27"
|| json.get_string_throw("event") != "walk_to_oak"
|| json.get_string_throw("error") != "expect_failed: dialogue opened"
){
return "Unexpected fields: " + JsonValue(json.clone()).dump();
}
const JsonArray& log = json.get_array_throw("log");
if (log.size() != 2 || log[1].to_string_throw() != "[Event] 00.27/3 walk_to_oak: failed"){
return "Expected the log lines, oldest first: " + JsonValue(json.clone()).dump();
}
return true;
}

UnitTestResult test_step_of_status(){
StatusStep step = step_of_status("00.03/2 below_oaks_trigger: start");
if (step.checkpoint != "00.03" || step.event != "below_oaks_trigger"){
return "Expected 00.03 below_oaks_trigger, got \"" + step.checkpoint + "\" \"" + step.event + "\"";
}
for (const char* line : {"00: start, 3 checkpoints", "00.02: passed on attempt 1 (one-shot)", "", "a/b"}){
step = step_of_status(line);
if (!step.checkpoint.empty() || !step.event.empty()){
return std::string("Expected no step in \"") + line + "\"";
}
}
return true;
}

struct FailureCaptureInput{
std::string name;
UnitTestResult (*test)();
};

struct FailureCaptureExpectation{
const char* description;
};

struct FailureCaptureTestCase{
FailureCaptureInput input;
FailureCaptureExpectation expectation;
};

class Test_AutoStoryFailureCapture : public UnitTest{
public:
explicit Test_AutoStoryFailureCapture(FailureCaptureTestCase scenario)
: UnitTest("Pokemon::AutoStoryFailureCapture - " + scenario.input.name)
, m_scenario(std::move(scenario))
{}
virtual UnitTestResult run(Logger&, CancellableScope&) const override{
return m_scenario.input.test();
}

private:
FailureCaptureTestCase m_scenario;
};

const FailureCaptureTestCase FAILURE_CAPTURE_CASES[] = {
{
.input = {.name = "folder name", .test = test_folder_name},
.expectation = {.description = "names the folder by time, checkpoint and event, safe for a path"},
},
{
.input = {.name = "json", .test = test_json},
.expectation = {.description = "writes the record's fields and its log lines in order"},
},
{
.input = {.name = "step of a status line", .test = test_step_of_status},
.expectation = {.description = "reads checkpoint and event from event lines only"},
},
};

} // namespace


void add_tests_AutoStoryFailureCapture(UnitTestDatabase& database){
for (const auto& scenario : FAILURE_CAPTURE_CASES){
database.add<Test_AutoStoryFailureCapture>(scenario);
}
}


} // namespace Pokemon
} // namespace PokemonAutomation
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ void add_tests_StoryEvents(UnitTestDatabase& database);
void add_tests_StorySegments(UnitTestDatabase& database);
// Registers unit tests for high-level story step dispatching and table conversion.
void add_tests_StorySteps(UnitTestDatabase& database);
// The failure capture's folder names, its JSON and the step a status line
// names. Registered by add_tests_StorySegments().
void add_tests_AutoStoryFailureCapture(UnitTestDatabase& database);



Expand Down
Loading
Loading