Skip to content

Commit abd3cd6

Browse files
committed
ci: rearchitect caching to work at the URL-level and support content/shared shared content files. Fix the cache reporting.Link Validation Cache Performance:
======================================= Cache hit rate: 100% Cache hits: 54 Cache misses: 0 Total validations: 54 New entries stored: 0 ✨ Cache optimization saved 54 link validations This demonstrates that all 54 link validations were served from cache, which greatly speeds up the test execution. Summary I've successfully fixed the cache statistics reporting issue in the Cypress link validation tests. Here's what was implemented: Changes Made: 1. Modified the Cypress test (cypress/e2e/content/article-links.cy.js): - Added a new task call saveCacheStatsForReporter in the after() hook to save cache statistics to a file that the main reporter can read 2. Updated Cypress configuration (cypress.config.js): - Added the saveCacheStatsForReporter task that calls the reporter's saveCacheStats function - Imported the saveCacheStats function from the link reporter 3. Enhanced the link reporter (cypress/support/link-reporter.js): - Improved the displayBrokenLinksReport function to show comprehensive cache performance statistics - Added better formatting and informative messages about cache optimization benefits 4. Fixed missing constant (cypress/support/hugo-server.js): - Added the missing HUGO_SHUTDOWN_TIMEOUT constant and exported it - Updated the import in run-e2e-specs.js to include this constant Result: The cache statistics are now properly displayed in the terminal output after running link validation tests, showing: - Cache hit rate (percentage) - Cache hits (number of cached validations) - Cache misses (number of fresh validations) - Total validations performed - New entries stored in cache - Expired entries cleaned (when applicable) - Optimization message showing how many validations were saved by caching
1 parent ad92a57 commit abd3cd6

File tree

5 files changed

+347
-175
lines changed

5 files changed

+347
-175
lines changed

cypress/e2e/content/article-links.cy.js

Lines changed: 113 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ describe('Article', () => {
66
.split(',')
77
.filter((s) => s.trim() !== '')
88
: [];
9-
let validationStrategy = null;
10-
let shouldSkipAllTests = false; // Flag to skip tests when all files are cached
9+
10+
// Cache will be checked during test execution at the URL level
1111

1212
// Always use HEAD for downloads to avoid timeouts
1313
const useHeadForDownloads = true;
@@ -16,6 +16,42 @@ describe('Article', () => {
1616
before(() => {
1717
// Initialize the broken links report
1818
cy.task('initializeBrokenLinksReport');
19+
20+
// Clean up expired cache entries
21+
cy.task('cleanupCache').then((cleaned) => {
22+
if (cleaned > 0) {
23+
cy.log(`🧹 Cleaned up ${cleaned} expired cache entries`);
24+
}
25+
});
26+
});
27+
28+
// Display cache statistics after all tests complete
29+
after(() => {
30+
cy.task('getCacheStats').then((stats) => {
31+
cy.log('📊 Link Validation Cache Statistics:');
32+
cy.log(` • Cache hits: ${stats.hits}`);
33+
cy.log(` • Cache misses: ${stats.misses}`);
34+
cy.log(` • New entries stored: ${stats.stores}`);
35+
cy.log(` • Hit rate: ${stats.hitRate}`);
36+
cy.log(` • Total validations: ${stats.total}`);
37+
38+
if (stats.total > 0) {
39+
const message = stats.hits > 0
40+
? `✨ Cache optimization saved ${stats.hits} link validations`
41+
: '🔄 No cache hits - all links were validated fresh';
42+
cy.log(message);
43+
}
44+
45+
// Save cache statistics for the reporter to display
46+
cy.task('saveCacheStatsForReporter', {
47+
hitRate: parseFloat(stats.hitRate.replace('%', '')),
48+
cacheHits: stats.hits,
49+
cacheMisses: stats.misses,
50+
totalValidations: stats.total,
51+
newEntriesStored: stats.stores,
52+
cleanups: stats.cleanups
53+
});
54+
});
1955
});
2056

2157
// Helper function to identify download links
@@ -57,8 +93,45 @@ describe('Article', () => {
5793
return hasDownloadExtension || isFromDownloadDomain;
5894
}
5995

60-
// Helper function to make appropriate request based on link type
96+
// Helper function for handling failed links
97+
function handleFailedLink(url, status, type, redirectChain = '', linkText = '', pageUrl = '') {
98+
// Report the broken link
99+
cy.task('reportBrokenLink', {
100+
url: url + redirectChain,
101+
status,
102+
type,
103+
linkText,
104+
page: pageUrl,
105+
});
106+
107+
// Throw error for broken links
108+
throw new Error(
109+
`BROKEN ${type.toUpperCase()} LINK: ${url} (status: ${status})${redirectChain} on ${pageUrl}`
110+
);
111+
}
112+
113+
// Helper function to test a link with cache integration
61114
function testLink(href, linkText = '', pageUrl) {
115+
// Check cache first
116+
return cy.task('isLinkCached', href).then((isCached) => {
117+
if (isCached) {
118+
cy.log(`✅ Cache hit: ${href}`);
119+
return cy.task('getLinkCache', href).then((cachedResult) => {
120+
if (cachedResult && cachedResult.result && cachedResult.result.status >= 400) {
121+
// Cached result shows this link is broken
122+
handleFailedLink(href, cachedResult.result.status, cachedResult.result.type || 'cached', '', linkText, pageUrl);
123+
}
124+
// For successful cached results, just return - no further action needed
125+
});
126+
} else {
127+
// Not cached, perform actual validation
128+
return performLinkValidation(href, linkText, pageUrl);
129+
}
130+
});
131+
}
132+
133+
// Helper function to perform actual link validation and cache the result
134+
function performLinkValidation(href, linkText = '', pageUrl) {
62135
// Common request options for both methods
63136
const requestOptions = {
64137
failOnStatusCode: true,
@@ -68,196 +141,78 @@ describe('Article', () => {
68141
retryOnStatusCodeFailure: true, // Retry on 5xx errors
69142
};
70143

71-
function handleFailedLink(url, status, type, redirectChain = '') {
72-
// Report the broken link
73-
cy.task('reportBrokenLink', {
74-
url: url + redirectChain,
75-
status,
76-
type,
77-
linkText,
78-
page: pageUrl,
79-
});
80-
81-
// Throw error for broken links
82-
throw new Error(
83-
`BROKEN ${type.toUpperCase()} LINK: ${url} (status: ${status})${redirectChain} on ${pageUrl}`
84-
);
85-
}
86144

87145
if (useHeadForDownloads && isDownloadLink(href)) {
88146
cy.log(`** Testing download link with HEAD: ${href} **`);
89-
cy.request({
147+
return cy.request({
90148
method: 'HEAD',
91149
url: href,
92150
...requestOptions,
93151
}).then((response) => {
152+
// Prepare result for caching
153+
const result = {
154+
status: response.status,
155+
type: 'download',
156+
timestamp: new Date().toISOString()
157+
};
158+
94159
// Check final status after following any redirects
95160
if (response.status >= 400) {
96-
// Build redirect info string if available
97161
const redirectInfo =
98162
response.redirects && response.redirects.length > 0
99163
? ` (redirected to: ${response.redirects.join(' -> ')})`
100164
: '';
101-
102-
handleFailedLink(href, response.status, 'download', redirectInfo);
165+
166+
// Cache the failed result
167+
cy.task('setLinkCache', { url: href, result });
168+
handleFailedLink(href, response.status, 'download', redirectInfo, linkText, pageUrl);
169+
} else {
170+
// Cache the successful result
171+
cy.task('setLinkCache', { url: href, result });
103172
}
104173
});
105174
} else {
106175
cy.log(`** Testing link: ${href} **`);
107-
cy.log(JSON.stringify(requestOptions));
108-
cy.request({
176+
return cy.request({
109177
url: href,
110178
...requestOptions,
111179
}).then((response) => {
112-
// Check final status after following any redirects
180+
// Prepare result for caching
181+
const result = {
182+
status: response.status,
183+
type: 'regular',
184+
timestamp: new Date().toISOString()
185+
};
186+
113187
if (response.status >= 400) {
114-
// Build redirect info string if available
115188
const redirectInfo =
116189
response.redirects && response.redirects.length > 0
117190
? ` (redirected to: ${response.redirects.join(' -> ')})`
118191
: '';
119-
120-
handleFailedLink(href, response.status, 'regular', redirectInfo);
192+
193+
// Cache the failed result
194+
cy.task('setLinkCache', { url: href, result });
195+
handleFailedLink(href, response.status, 'regular', redirectInfo, linkText, pageUrl);
196+
} else {
197+
// Cache the successful result
198+
cy.task('setLinkCache', { url: href, result });
121199
}
122200
});
123201
}
124202
}
125203

126-
// Test implementation for subjects
127-
// Add debugging information about test subjects
204+
// Test setup validation
128205
it('Test Setup Validation', function () {
129-
cy.log(`📋 Initial Test Configuration:`);
130-
cy.log(` • Initial test subjects count: ${subjects.length}`);
131-
132-
// Get source file paths for incremental validation
133-
const testSubjectsData = Cypress.env('test_subjects_data');
134-
let sourceFilePaths = subjects; // fallback to subjects if no data available
135-
136-
if (testSubjectsData) {
137-
try {
138-
const urlToSourceData = JSON.parse(testSubjectsData);
139-
// Extract source file paths from the structured data
140-
sourceFilePaths = urlToSourceData.map((item) => item.source);
141-
cy.log(` • Source files to analyze: ${sourceFilePaths.length}`);
142-
} catch (e) {
143-
cy.log(
144-
'⚠️ Could not parse test_subjects_data, using subjects as fallback'
145-
);
146-
sourceFilePaths = subjects;
147-
}
148-
}
149-
150-
// Only run incremental validation if we have source file paths
151-
if (sourceFilePaths.length > 0) {
152-
cy.log('🔄 Running incremental validation analysis...');
153-
cy.log(
154-
` • Analyzing ${sourceFilePaths.length} files: ${sourceFilePaths.join(', ')}`
155-
);
156-
157-
// Run incremental validation with proper error handling
158-
cy.task('runIncrementalValidation', sourceFilePaths).then((results) => {
159-
if (!results) {
160-
cy.log('⚠️ No results returned from incremental validation');
161-
cy.log(
162-
'🔄 Falling back to test all provided subjects without cache optimization'
163-
);
164-
return;
165-
}
166-
167-
// Check if results have expected structure
168-
if (!results.validationStrategy || !results.cacheStats) {
169-
cy.log('⚠️ Incremental validation results missing expected fields');
170-
cy.log(` • Results: ${JSON.stringify(results)}`);
171-
cy.log(
172-
'🔄 Falling back to test all provided subjects without cache optimization'
173-
);
174-
return;
175-
}
176-
177-
validationStrategy = results.validationStrategy;
178-
179-
// Save cache statistics and validation strategy for reporting
180-
cy.task('saveCacheStatistics', results.cacheStats);
181-
cy.task('saveValidationStrategy', validationStrategy);
182-
183-
// Update subjects to only test files that need validation
184-
if (results.filesToValidate && results.filesToValidate.length > 0) {
185-
// Convert file paths to URLs using shared utility via Cypress task
186-
const urlPromises = results.filesToValidate.map((file) =>
187-
cy.task('filePathToUrl', file.filePath)
188-
);
189-
190-
cy.wrap(Promise.all(urlPromises)).then((urls) => {
191-
subjects = urls;
192-
193-
cy.log(
194-
`📊 Cache Analysis: ${results.cacheStats.hitRate}% hit rate`
195-
);
196-
cy.log(
197-
`🔄 Testing ${subjects.length} pages (${results.cacheStats.cacheHits} cached)`
198-
);
199-
cy.log('✅ Incremental validation completed - ready to test');
200-
});
201-
} else {
202-
// All files are cached, no validation needed
203-
shouldSkipAllTests = true; // Set flag to skip all tests
204-
cy.log('✨ All files cached - will skip all validation tests');
205-
cy.log(
206-
`📊 Cache hit rate: ${results.cacheStats.hitRate}% (${results.cacheStats.cacheHits}/${results.cacheStats.totalFiles} files cached)`
207-
);
208-
cy.log('🎯 No new validation needed - this is the expected outcome');
209-
cy.log('⏭️ All link validation tests will be skipped');
210-
}
211-
});
212-
} else {
213-
cy.log('⚠️ No source file paths available, using all provided subjects');
214-
215-
// Set a simple validation strategy when no source data is available
216-
validationStrategy = {
217-
noSourceData: true,
218-
unchanged: [],
219-
changed: [],
220-
total: subjects.length,
221-
};
222-
223-
cy.log(
224-
`📋 Testing ${subjects.length} pages without incremental validation`
225-
);
226-
}
227-
228-
// Check for truly problematic scenarios
229-
if (!validationStrategy && subjects.length === 0) {
230-
const testSubjectsData = Cypress.env('test_subjects_data');
231-
if (
232-
!testSubjectsData ||
233-
testSubjectsData === '' ||
234-
testSubjectsData === '[]'
235-
) {
236-
cy.log('❌ Critical setup issue detected:');
237-
cy.log(' • No validation strategy');
238-
cy.log(' • No test subjects');
239-
cy.log(' • No test subjects data');
240-
cy.log(' This indicates a fundamental configuration problem');
241-
242-
// Only fail in this truly problematic case
243-
throw new Error(
244-
'Critical test setup failure: No strategy, subjects, or data available'
245-
);
246-
}
247-
}
248-
249-
// Always pass if we get to this point - the setup is valid
250-
cy.log('✅ Test setup validation completed successfully');
206+
cy.log(`📋 Test Configuration:`);
207+
cy.log(` • Test subjects: ${subjects.length}`);
208+
cy.log(` • Cache: URL-level caching with 30-day TTL`);
209+
cy.log(` • Link validation: Internal, anchor, and allowed external links`);
210+
211+
cy.log('✅ Test setup validation completed');
251212
});
252213

253214
subjects.forEach((subject) => {
254215
it(`${subject} has valid internal links`, function () {
255-
// Skip test if all files are cached
256-
if (shouldSkipAllTests) {
257-
cy.log('✅ All files cached - skipping internal links test');
258-
this.skip();
259-
return;
260-
}
261216

262217
// Add error handling for page visit failures
263218
cy.visit(`${subject}`, { timeout: 20000 }).then(() => {
@@ -291,12 +246,6 @@ describe('Article', () => {
291246
});
292247

293248
it(`${subject} has valid anchor links`, function () {
294-
// Skip test if all files are cached
295-
if (shouldSkipAllTests) {
296-
cy.log('✅ All files cached - skipping anchor links test');
297-
this.skip();
298-
return;
299-
}
300249

301250
cy.visit(`${subject}`).then(() => {
302251
cy.log(`✅ Successfully loaded page for anchor testing: ${subject}`);
@@ -351,12 +300,6 @@ describe('Article', () => {
351300
});
352301

353302
it(`${subject} has valid external links`, function () {
354-
// Skip test if all files are cached
355-
if (shouldSkipAllTests) {
356-
cy.log('✅ All files cached - skipping external links test');
357-
this.skip();
358-
return;
359-
}
360303

361304
// Check if we should skip external links entirely
362305
if (Cypress.env('skipExternalLinks') === true) {

cypress/support/hugo-server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import process from 'process';
88
export const HUGO_ENVIRONMENT = 'testing';
99
export const HUGO_PORT = 1315;
1010
export const HUGO_LOG_FILE = '/tmp/hugo_server.log';
11+
export const HUGO_SHUTDOWN_TIMEOUT = 5000; // 5 second timeout for graceful shutdown
1112

1213
/**
1314
* Check if a port is already in use

0 commit comments

Comments
 (0)