Skip to content

Commit 6f78b9e

Browse files
authored
Merge branch 'master' into influxql-integral
2 parents 31e6b5c + c781182 commit 6f78b9e

File tree

5 files changed

+349
-175
lines changed

5 files changed

+349
-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)