Files
TheRock/rocm-libraries/shared/stinkytofu/tools/stinkytofu-check/stinkytofu-check.cpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

419 lines
16 KiB
C++

/* ************************************************************************
* Copyright (C) 2026 Advanced Micro Devices, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* ************************************************************************ */
/// stinkytofu-check: LLVM FileCheck-style test runner for stinkytofu-opt.
///
/// Reads a .stir file containing:
/// # RUN: %stinkytofu-opt --arch gfx1250 %s --SomePass --print-output
/// # CHECK: expected_substring
/// # CHECK-NEXT: must_be_on_next_line
/// # CHECK-NOT: must_not_appear
/// # XFAIL: the command is expected to exit non-zero; CHECK patterns still verified
///
/// Executes the RUN command, captures stdout, and verifies CHECK directives.
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#endif
#include <cstring>
#include <fstream>
#include <iostream>
#include <regex>
#include <sstream>
#include <string>
#include <vector>
namespace {
// Match a pattern against a line. Patterns can contain {{regex}} sections.
// Plain text is matched literally, {{...}} sections are matched as regex.
// Example: "ds_load_b128{{.*}}v[8:11]" matches " v[8:11] = ds_load_b128(v24)"
bool matchPattern(const std::string& line, const std::string& pattern) {
// If no {{ in pattern, use simple substring match.
if (pattern.find("{{") == std::string::npos) return line.find(pattern) != std::string::npos;
// Convert pattern to regex: literal parts are escaped, {{...}} becomes regex.
std::string regexStr;
size_t pos = 0;
while (pos < pattern.size()) {
size_t start = pattern.find("{{", pos);
if (start == std::string::npos) {
// Remaining is literal — escape regex special chars.
for (size_t i = pos; i < pattern.size(); ++i) {
if (std::string("[](){}.*+?^$\\|").find(pattern[i]) != std::string::npos)
regexStr += '\\';
regexStr += pattern[i];
}
break;
}
// Literal part before {{
for (size_t i = pos; i < start; ++i) {
if (std::string("[](){}.*+?^$\\|").find(pattern[i]) != std::string::npos)
regexStr += '\\';
regexStr += pattern[i];
}
// Find closing }}
size_t end = pattern.find("}}", start + 2);
if (end == std::string::npos) break; // Malformed — treat rest as literal
// Extract regex between {{ and }}
regexStr += pattern.substr(start + 2, end - start - 2);
pos = end + 2;
}
std::regex re(regexStr, std::regex::nosubs);
return std::regex_search(line, re);
}
struct CheckDirective {
enum Kind { CHECK, CHECK_NEXT, CHECK_SAME, CHECK_NOT, CHECK_DAG, CHECK_LABEL };
Kind kind;
std::string pattern;
int lineNum;
};
// Parse # RUN:, # XFAIL, and # CHECK: directives from the test file.
bool parseTestFile(const std::string& filename, std::string& runCmd,
std::vector<CheckDirective>& checks, bool& xfail) {
std::ifstream file(filename);
if (!file.is_open()) {
std::cerr << "Error: cannot open " << filename << "\n";
return false;
}
xfail = false;
std::string line;
int lineNum = 0;
while (std::getline(file, line)) {
++lineNum;
// Skip leading whitespace
size_t pos = line.find_first_not_of(" \t");
if (pos == std::string::npos) continue;
// Lines starting with # (comments in .stir)
if (line[pos] != '#') continue;
std::string rest = line.substr(pos + 1);
pos = rest.find_first_not_of(" \t");
if (pos == std::string::npos) continue;
rest = rest.substr(pos);
if (rest.starts_with("XFAIL")) {
xfail = true;
} else if (rest.starts_with("RUN:")) {
runCmd = rest.substr(4);
// Trim leading whitespace
pos = runCmd.find_first_not_of(" \t");
if (pos != std::string::npos) runCmd = runCmd.substr(pos);
} else if (rest.starts_with("CHECK-LABEL:")) {
std::string pat = rest.substr(12);
pos = pat.find_first_not_of(" \t");
if (pos != std::string::npos) pat = pat.substr(pos);
checks.push_back({CheckDirective::CHECK_LABEL, pat, lineNum});
} else if (rest.starts_with("CHECK-SAME:")) {
std::string pat = rest.substr(11);
pos = pat.find_first_not_of(" \t");
if (pos != std::string::npos) pat = pat.substr(pos);
checks.push_back({CheckDirective::CHECK_SAME, pat, lineNum});
} else if (rest.starts_with("CHECK-NEXT:")) {
std::string pat = rest.substr(11);
pos = pat.find_first_not_of(" \t");
if (pos != std::string::npos) pat = pat.substr(pos);
checks.push_back({CheckDirective::CHECK_NEXT, pat, lineNum});
} else if (rest.starts_with("CHECK-DAG:")) {
std::string pat = rest.substr(10);
pos = pat.find_first_not_of(" \t");
if (pos != std::string::npos) pat = pat.substr(pos);
checks.push_back({CheckDirective::CHECK_DAG, pat, lineNum});
} else if (rest.starts_with("CHECK-NOT:")) {
std::string pat = rest.substr(10);
pos = pat.find_first_not_of(" \t");
if (pos != std::string::npos) pat = pat.substr(pos);
checks.push_back({CheckDirective::CHECK_NOT, pat, lineNum});
} else if (rest.starts_with("CHECK:")) {
std::string pat = rest.substr(6);
pos = pat.find_first_not_of(" \t");
if (pos != std::string::npos) pat = pat.substr(pos);
checks.push_back({CheckDirective::CHECK, pat, lineNum});
}
}
return !runCmd.empty();
}
// Execute a command and capture stdout. Returns the exit status via out-param.
std::string executeCommand(const std::string& cmd, int& exitStatus) {
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
std::cerr << "Error: failed to execute: " << cmd << "\n";
exitStatus = -1;
return {};
}
std::string result;
char buffer[4096];
while (fgets(buffer, sizeof(buffer), pipe)) result += buffer;
exitStatus = pclose(pipe);
return result;
}
// Substitute %s and %stinkytofu-opt in the RUN command.
std::string substituteVars(const std::string& cmd, const std::string& testFile,
const std::string& optBinary) {
std::string result = cmd;
// Replace %stinkytofu-opt first (longer match before %s)
size_t pos;
while ((pos = result.find("%stinkytofu-opt")) != std::string::npos)
result.replace(pos, 15, optBinary);
while ((pos = result.find("%s")) != std::string::npos) result.replace(pos, 2, testFile);
return result;
}
// Verify CHECK directives against output lines.
bool verifyChecks(const std::vector<std::string>& outputLines,
const std::vector<CheckDirective>& checks, const std::string& testFile) {
size_t outputIdx = 0;
bool ok = true;
for (size_t i = 0; i < checks.size(); ++i) {
const auto& chk = checks[i];
if (chk.kind == CheckDirective::CHECK_LABEL) {
// Reset scan position — find pattern from the beginning.
bool found = false;
for (size_t j = 0; j < outputLines.size(); ++j) {
if (matchPattern(outputLines[j], chk.pattern)) {
outputIdx = j + 1;
found = true;
break;
}
}
if (!found) {
std::cerr << testFile << ":" << chk.lineNum
<< ": CHECK-LABEL failed: pattern not found\n";
std::cerr << " expected: " << chk.pattern << "\n";
ok = false;
}
continue;
}
if (chk.kind == CheckDirective::CHECK_SAME) {
// Match on the same line as the previous CHECK (outputIdx-1).
if (outputIdx == 0) {
std::cerr << testFile << ":" << chk.lineNum
<< ": CHECK-SAME failed: no previous CHECK\n";
ok = false;
continue;
}
size_t prevIdx = outputIdx - 1;
if (!matchPattern(outputLines[prevIdx], chk.pattern)) {
std::cerr << testFile << ":" << chk.lineNum << ": CHECK-SAME failed at output line "
<< (prevIdx + 1) << "\n";
std::cerr << " expected: " << chk.pattern << "\n";
std::cerr << " got: " << outputLines[prevIdx] << "\n";
ok = false;
}
continue;
}
if (chk.kind == CheckDirective::CHECK_NOT) {
// Pattern must NOT appear before the next CHECK/CHECK-LABEL.
// Find scan end: the output line matched by the next non-NOT directive.
size_t scanEnd = outputLines.size();
for (size_t j = i + 1; j < checks.size(); ++j) {
if (checks[j].kind == CheckDirective::CHECK_NOT) continue;
// For CHECK-LABEL, find its match position to bound the scan.
if (checks[j].kind == CheckDirective::CHECK_LABEL) {
for (size_t k = 0; k < outputLines.size(); ++k) {
if (matchPattern(outputLines[k], checks[j].pattern)) {
scanEnd = k;
break;
}
}
} else {
// For other CHECK directives, scan forward from current pos.
for (size_t k = outputIdx; k < outputLines.size(); ++k) {
if (matchPattern(outputLines[k], checks[j].pattern)) {
scanEnd = k;
break;
}
}
}
break;
}
for (size_t j = outputIdx; j < scanEnd; ++j) {
if (matchPattern(outputLines[j], chk.pattern)) {
std::cerr << testFile << ":" << chk.lineNum << ": CHECK-NOT failed: found '"
<< chk.pattern << "' at output line " << (j + 1) << "\n";
std::cerr << " output: " << outputLines[j] << "\n";
ok = false;
break;
}
}
continue;
}
if (chk.kind == CheckDirective::CHECK_NEXT) {
if (outputIdx >= outputLines.size()) {
std::cerr << testFile << ":" << chk.lineNum
<< ": CHECK-NEXT failed: no more output\n";
std::cerr << " expected: " << chk.pattern << "\n";
ok = false;
continue;
}
if (!matchPattern(outputLines[outputIdx], chk.pattern)) {
std::cerr << testFile << ":" << chk.lineNum << ": CHECK-NEXT failed at output line "
<< (outputIdx + 1) << "\n";
std::cerr << " expected: " << chk.pattern << "\n";
std::cerr << " got: " << outputLines[outputIdx] << "\n";
ok = false;
}
++outputIdx;
continue;
}
if (chk.kind == CheckDirective::CHECK_DAG) {
// Collect consecutive CHECK-DAG directives into a group.
std::vector<const CheckDirective*> dagGroup;
dagGroup.push_back(&chk);
while (i + 1 < checks.size() && checks[i + 1].kind == CheckDirective::CHECK_DAG)
dagGroup.push_back(&checks[++i]);
// All patterns in the group must match within the output range
// [outputIdx, outputIdx + scanRange), in any order.
// Each pattern consumes one output line.
std::vector<bool> matched(dagGroup.size(), false);
size_t startIdx = outputIdx;
size_t endIdx = outputLines.size();
for (size_t g = 0; g < dagGroup.size(); ++g) {
bool found = false;
for (size_t j = startIdx; j < endIdx; ++j) {
if (matchPattern(outputLines[j], dagGroup[g]->pattern)) {
matched[g] = true;
found = true;
break;
}
}
if (!found) {
std::cerr << testFile << ":" << dagGroup[g]->lineNum
<< ": CHECK-DAG failed: pattern not found\n";
std::cerr << " expected: " << dagGroup[g]->pattern << "\n";
ok = false;
}
}
// Advance outputIdx past all matched lines.
// Find the furthest matched line.
for (size_t j = startIdx; j < endIdx; ++j) {
for (size_t g = 0; g < dagGroup.size(); ++g) {
if (matched[g] && matchPattern(outputLines[j], dagGroup[g]->pattern)) {
if (j + 1 > outputIdx) outputIdx = j + 1;
}
}
}
continue;
}
// CHECK: scan forward for the pattern.
bool found = false;
for (; outputIdx < outputLines.size(); ++outputIdx) {
if (matchPattern(outputLines[outputIdx], chk.pattern)) {
found = true;
++outputIdx;
break;
}
}
if (!found) {
std::cerr << testFile << ":" << chk.lineNum << ": CHECK failed: pattern not found\n";
std::cerr << " expected: " << chk.pattern << "\n";
ok = false;
}
}
return ok;
}
} // namespace
int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "Usage: stinkytofu-check <test.stir> [--stinkytofu-opt <path>]\n";
return 1;
}
std::string testFile = argv[1];
std::string optBinary = "stinkytofu-opt"; // default
for (int i = 2; i < argc; ++i) {
if (std::string(argv[i]) == "--stinkytofu-opt" && i + 1 < argc) optBinary = argv[++i];
}
// Parse test file
std::string runCmd;
std::vector<CheckDirective> checks;
bool xfail = false;
if (!parseTestFile(testFile, runCmd, checks, xfail)) {
std::cerr << "Error: no RUN: directive found in " << testFile << "\n";
return 1;
}
if (checks.empty()) {
std::cerr << "Error: no CHECK directives found in " << testFile << "\n";
return 1;
}
// Substitute variables and execute
std::string cmd = substituteVars(runCmd, testFile, optBinary);
int exitStatus = 0;
std::string output = executeCommand(cmd, exitStatus);
if (exitStatus != 0) {
if (xfail) {
std::cerr << testFile << ": note: command exited non-zero (expected by XFAIL)\n";
} else {
std::cerr << "Warning: command exited with status " << exitStatus << "\n";
}
}
// Split output into lines
std::vector<std::string> outputLines;
{
std::istringstream stream(output);
std::string line;
while (std::getline(stream, line)) outputLines.push_back(line);
}
// Verify
if (verifyChecks(outputLines, checks, testFile)) {
std::cerr << testFile << ": PASSED\n";
return 0;
} else {
std::cerr << testFile << ": FAILED\n";
return 1;
}
}