Skip to content
Merged
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
7 changes: 6 additions & 1 deletion dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ async function main() {
name: "firefox",
headless: false
}]
}]
}],
integrations: {
docDetectiveApi: {
apiKey: process.env.KEY
}
}
};
// console.log(json);
result = await runTests(json);
Expand Down
39 changes: 30 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const { detectTests, detectAndResolveTests } = require("doc-detective-resolver");
const {
detectAndResolveTests,
} = require("doc-detective-resolver");
const { log, cleanTemp } = require("./utils");
const { runSpecs } = require("./tests");
const { runSpecs, runViaApi } = require("./tests");
const { telemetryNotice, sendTelemetry } = require("./telem");

exports.runTests = runTests;
Expand All @@ -14,18 +16,37 @@ const supportMessage = `
##########################################################################`;

// Run tests defined in specifications and documentation source files.
async function runTests(config) {
async function runTests(config, options = {}) {
let resolvedTests;
let results;

if (options.resolvedTests) {
resolvedTests = options.resolvedTests;
config = resolvedTests.config;
}

// Telemetry notice
telemetryNotice(config);

const resolvedTests = await detectAndResolveTests({ config });
if (!resolvedTests || resolvedTests.specs.length === 0) {
log(config, "warn", "Couldn't resolve any tests.");
return null;
if (!resolvedTests) {
resolvedTests = await detectAndResolveTests({ config });
if (!resolvedTests || resolvedTests.specs.length === 0) {
log(config, "warn", "Couldn't resolve any tests.");
return null;
}
}

// Run test specs
const results = await runSpecs({ resolvedTests });
// If config.integrations.docDetectiveApi.apiKey is set, run tests via API instead of locally
if (config.integrations && config.integrations.docDetectiveApi && config.integrations.docDetectiveApi.apiKey) {
// Run test specs via API
results = await runViaApi({
resolvedTests,
apiKey: config.integrations.docDetectiveApi.apiKey,
});
} else {
// Run test specs locally
results = await runSpecs({ resolvedTests });
}
log(config, "info", "RESULTS:");
log(config, "info", results);
log(config, "info", "Cleaning up and finishing post-processing.");
Expand Down
104 changes: 104 additions & 0 deletions src/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const { resolveExpression } = require("./expressions");
const { getEnvironment, getAvailableApps } = require("./config");

exports.runSpecs = runSpecs;
exports.runViaApi = runViaApi;
// exports.appiumStart = appiumStart;
// exports.appiumIsReady = appiumIsReady;
// exports.driverStart = driverStart;
Expand Down Expand Up @@ -238,6 +239,109 @@ async function allowUnsafeSteps({ config }) {
else return false;
}

// Run specifications via API.
async function runViaApi({ resolvedTests, apiKey, config = {} }) {
const baseUrl =
process.env.DOC_DETECTIVE_API_URL || "https://api.doc-detective.com/v1";
// Make an API request to create a test run
const apiUrl = `${baseUrl}/runs`;

// Configure axios with proper timeout and connection handling
const axiosConfig = {
headers: {
"X-API-Key": apiKey,
"Content-Type": "application/json",
},
// Prevent connection reuse issues with keep-alive
httpAgent: new (require("http").Agent)({ keepAlive: false }),
httpsAgent: new (require("https").Agent)({ keepAlive: false }),
};

// Create run
let createResponse;
try {
createResponse = await axios.post(apiUrl, resolvedTests, axiosConfig);
} catch (error) {
return { status: error.response?.status, error: error.response?.data?.error };
}
if (createResponse.status !== 201) {
return { status: createResponse.status, error: createResponse.data.error };
}
const runId = createResponse.data.run.runId;

// TODO: Add file uploads, if any

// Start run
let startResponse;
try {
startResponse = await axios.post(
`${apiUrl}/${runId}/start`,
{},
axiosConfig
);
} catch (error) {
return { status: error.response?.status, error: error.response?.data?.error };
}
if (startResponse.status !== 200) {
return { status: startResponse.status, error: startResponse.data.error };
}

// Poll for results
const pollInterval = 5000; // 5 seconds in milliseconds
const pollIntervalVariance = 2000; // +/- 2 seconds
const maxWaitTime = (config.apiMaxWaitTime || 600) * 1000; // Default 600 seconds (10 minutes), converted to milliseconds
const startTime = Date.now();

let response;
while (true) {
// Check if we've exceeded the max wait time
if (Date.now() - startTime > maxWaitTime) {
return {
status: 408,
type: "TIMEOUT",
error: `Test execution exceeded maximum wait time of ${maxWaitTime / 1000} seconds`,
};
}

// Poll for results
try {
response = await axios.get(`${apiUrl}/${runId}`, axiosConfig);
} catch (error) {
return {
status: error.response?.status,
error: error.response?.data?.error,
};
Comment thread
hawkeyexl marked this conversation as resolved.
}

if (response.status !== 200) {
return { status: response.status, error: response.data.error };
}

// Check if the test run is complete
if (response.data.status === "completed") {
break;
}

// Wait before polling again (with variance)
const variance =
Math.random() * pollIntervalVariance * 2 - pollIntervalVariance;
const waitTime = pollInterval + variance;
await new Promise((resolve) => setTimeout(resolve, waitTime));
}

// TODO: Handle file downloads/placement, if any

try {
const results = JSON.parse(response.data.report);
return results;
} catch (error) {
return {
status: "PARSE_ERROR",
error: `Failed to parse API response: ${error.message}`,
};
}
}

/**
* Orchestrates execution of resolved test specifications and returns a hierarchical run report.
*
Expand Down
Loading