Cleaning up log files for C++ Approval Tests

I wrote about the Gilded Rose kata a while back (https://barneydellar.blogspot.com/2020/07/gilded-rose-katas.html) and one thing I looked at was the use of Approval Testing to get the code under control. 

And I went further, and investigated using the same library to test logging behaviour. 

Approval Testing relies on consistent output given consistent input. But logging code typically adds the date and time to the log file. This is desired behaviour, but it makes testing against the log file hard.

The C++ Approval Test library (https://github.com/approvals/ApprovalTests.cpp) always had support for dealing with this, but it was quite verbose. You had to create a derived TextFileComparator, override the contentsAreEquivalent() method, and then use regex to find and replace the date and time with something hard-coded.

class SpdLogComparator : public TextFileComparator {
public:
    [[nodiscard]]
    bool contentsAreEquivalent(
        std::string receivedPath,
        std::string approvedPath
    ) const override {
        spdlog::shutdown();
        const auto new_contents = MungedFileContents(receivedPath);
        WriteFileContents(receivedPath, new_contents);
        return TextFileComparator::contentsAreEquivalent(receivedPath, approvedPath);
    }
private:
    [[nodiscard]]
    std::string MungedFileContents(std::string receivedPath) const {
        std::stringstream new_contents;
        std::regex word_regex("[.*] [(.*)] [(.*)] (.*)");
        std::smatch sm;
        std::string line;
 
        std::ifstream rstream(receivedPath);
        while (std::getline(rstream, line)) {
            std::regex_match(line, sm, word_regex);
            if (sm.size() >= 2) {
                new_contents << "[TIME_AND_DATE MUNGED BY TESTS] [" << sm[1] << "] [" << sm[2].str() << "] " << sm[3].str() << '';
            } else {
                new_contents << line << '';
            }
        }
        return new_contents.str();
    }
 
    void WriteFileContents(std::string receivedPath, std::string new_contents) const {
        std::ofstream fileout(receivedPath, std::ifstream::out);
        fileout << new_contents;
    }
};

This works, but it's quite long winded. 

You also needed to register an instance of the comparator in order to use it:

TEST_CASE("Test log") {
    FileApprover::registerComparator(".log", std::make_shared< SpdLogComparator>());
    WriteLog();
    Approvals::verifyExistingFile(log_file);
}

But now the library has been improved a lot, and this is all much simpler. 

You can now just create a function that will return an ApprovalTests::Scrubber or an ApprovalTests::Options instance. The scrubber does the work of replacing the text using a regex, and the Options can be obtained from it, in order to specify the file extension to work on. This can be done in just a handful of lines, with no boilerplate:

inline ApprovalTests::Options Munger() {
    const std::string dateRegex = "\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}\\]";
    const std::string replacementText = "[MUNGED_DATE_TIME]";
    const auto munger = ApprovalTests::Scrubbers::createRegexScrubber(dateRegex, replacementText);
    return ApprovalTests::Options(munger).fileOptions().withFileExtension(".log");
}

And the use has been simplified too: 

TEST_CASE("Test log") {
    WriteLog();
    Approvals::verifyExistingFile(log_file, Munger());
}

This is a very nice improvement to the library, simplifying the test code a lot. Thanks to Clare Macrae for pointing this out to me :and helping me to get it working :-)

Comments

Popular posts from this blog

We always start from where we are

Mobodoro

Instant Legacy Code