Skip to content

Commit e492ad3

Browse files
Add config limits (#3626)
* Add config limits * Lint fix * add fake edit to check file output limit * Revert "add fake edit to check file output limit" This reverts commit 4bab93a. * Simplify
1 parent c35744f commit e492ad3

File tree

2 files changed

+339
-0
lines changed

2 files changed

+339
-0
lines changed

tests/feature-size-analysis.js

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { expect } from 'chai';
4+
import { CURRENT_CONFIG_VERSION } from '../constants.js';
5+
6+
/**
7+
* Feature size analysis tests. These tests analyze the size contribution of individual
8+
* features within config files and enforce growth limits.
9+
*/
10+
describe('Feature size analysis', () => {
11+
const GENERATED_DIR = path.join(import.meta.dirname, '..', 'generated');
12+
13+
const getConfigFiles = () => {
14+
if (!fs.existsSync(GENERATED_DIR)) {
15+
throw new Error('Generated directory does not exist. Run `npm run build` first.');
16+
}
17+
18+
// Use current version from constants
19+
const latestVersion = `v${CURRENT_CONFIG_VERSION}`;
20+
const versionDir = path.join(GENERATED_DIR, latestVersion);
21+
22+
if (!fs.existsSync(versionDir)) {
23+
throw new Error(`Version directory ${latestVersion} does not exist in generated/`);
24+
}
25+
const configFiles = fs.readdirSync(versionDir).filter((file) => file.endsWith('-config.json'));
26+
27+
return configFiles.map((file) => ({
28+
version: latestVersion,
29+
filename: file,
30+
filepath: path.join(versionDir, file),
31+
}));
32+
};
33+
34+
const analyzeFeatureSizes = (config) => {
35+
if (!config.features) {
36+
return [];
37+
}
38+
39+
const featureSizes = [];
40+
41+
Object.entries(config.features).forEach(
42+
([
43+
featureName,
44+
featureConfig,
45+
]) => {
46+
const featureJson = JSON.stringify(featureConfig);
47+
const sizeInBytes = Buffer.byteLength(featureJson, 'utf8');
48+
49+
featureSizes.push({
50+
name: featureName,
51+
size: sizeInBytes,
52+
sizeKB: (sizeInBytes / 1024).toFixed(2),
53+
config: featureConfig,
54+
});
55+
},
56+
);
57+
58+
// Sort by size descending
59+
return featureSizes.sort((a, b) => b.size - a.size);
60+
};
61+
62+
const findMaxFeatureSizes = () => {
63+
const configFiles = getConfigFiles();
64+
const maxSizes = {};
65+
66+
configFiles.forEach(({ filepath, filename }) => {
67+
const config = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
68+
const featureSizes = analyzeFeatureSizes(config);
69+
70+
featureSizes.forEach(({ name, size }) => {
71+
if (!maxSizes[name] || size > maxSizes[name].size) {
72+
maxSizes[name] = {
73+
size,
74+
platform: filename.replace('-config.json', ''),
75+
sizeKB: (size / 1024).toFixed(2),
76+
};
77+
}
78+
});
79+
});
80+
81+
return maxSizes;
82+
};
83+
84+
describe('Feature size reporting', () => {
85+
it('should analyze and report individual feature sizes across all platforms', () => {
86+
const configFiles = getConfigFiles();
87+
88+
console.log('\n=== Feature Size Analysis ===');
89+
90+
const allFeatureData = [];
91+
92+
configFiles.forEach(({ filepath, filename }) => {
93+
const config = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
94+
const featureSizes = analyzeFeatureSizes(config);
95+
const platform = filename.replace('-config.json', '');
96+
97+
console.log(`\n--- ${platform.toUpperCase()} ---`);
98+
console.log('Top 10 largest features:');
99+
100+
featureSizes.slice(0, 10).forEach((feature, index) => {
101+
console.log(
102+
`${(index + 1).toString().padStart(2)}. ${feature.name.padEnd(25)} ${feature.sizeKB.toString().padStart(8)}KB`,
103+
);
104+
});
105+
106+
allFeatureData.push({
107+
platform,
108+
features: featureSizes,
109+
});
110+
});
111+
112+
// Find max sizes across all platforms
113+
const maxSizes = findMaxFeatureSizes();
114+
115+
console.log('\n=== Maximum Feature Sizes Across All Platforms ===');
116+
const sortedMaxSizes = Object.entries(maxSizes)
117+
.map(
118+
([
119+
name,
120+
data,
121+
]) => ({ name, ...data }),
122+
)
123+
.sort((a, b) => b.size - a.size);
124+
125+
sortedMaxSizes.slice(0, 20).forEach((feature, index) => {
126+
console.log(
127+
`${(index + 1).toString().padStart(2)}. ${feature.name.padEnd(30)} ${feature.sizeKB.toString().padStart(8)}KB (${feature.platform})`,
128+
);
129+
});
130+
131+
// Find overall largest feature
132+
const largestFeature = sortedMaxSizes[0];
133+
if (largestFeature) {
134+
console.log(`\nLargest feature: ${largestFeature.name} at ${largestFeature.sizeKB}KB on ${largestFeature.platform}`);
135+
console.log(`10% growth allowance would be: ${(parseFloat(largestFeature.sizeKB) * 1.1).toFixed(2)}KB`);
136+
}
137+
138+
console.log('===============================\n');
139+
140+
// Store data for use in other tests
141+
global.featureAnalysisData = {
142+
maxSizes,
143+
largestFeature,
144+
allFeatureData,
145+
};
146+
147+
// Verify we have analysis data
148+
expect(Object.keys(maxSizes).length).to.be.greaterThan(0);
149+
});
150+
});
151+
152+
describe('Feature size growth limits', () => {
153+
let analysisData;
154+
155+
beforeEach(() => {
156+
// Run the analysis if not already done
157+
if (!global.featureAnalysisData) {
158+
const maxSizes = findMaxFeatureSizes();
159+
const sortedMaxSizes = Object.entries(maxSizes)
160+
.map(
161+
([
162+
name,
163+
data,
164+
]) => ({ name, ...data }),
165+
)
166+
.sort((a, b) => b.size - a.size);
167+
168+
global.featureAnalysisData = {
169+
maxSizes,
170+
largestFeature: sortedMaxSizes[0],
171+
};
172+
}
173+
analysisData = global.featureAnalysisData;
174+
});
175+
176+
it('should enforce platform-specific growth limits on the largest feature', function () {
177+
if (!analysisData.largestFeature) {
178+
this.skip();
179+
return;
180+
}
181+
182+
// Platform-specific base sizes for autoconsent feature
183+
const ANDROID_BASE_SIZE_KB = 310.2; // Current autoconsent size on Android
184+
const OTHER_PLATFORMS_BASE_SIZE_KB = 829.09; // Current autoconsent size on iOS/macOS/Windows
185+
186+
const configFiles = getConfigFiles();
187+
188+
configFiles.forEach(({ filepath, filename }) => {
189+
const config = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
190+
const featureSizes = analyzeFeatureSizes(config);
191+
const platform = filename.replace('-config.json', '');
192+
193+
// Determine base size and limit based on platform
194+
const isAndroid = platform === 'android';
195+
const baseSizeKB = isAndroid ? ANDROID_BASE_SIZE_KB : OTHER_PLATFORMS_BASE_SIZE_KB;
196+
const allowedSizeKB = baseSizeKB * 1.1; // 10% growth allowance
197+
const allowedSizeBytes = allowedSizeKB * 1024;
198+
199+
const autoconsentFeature = featureSizes.find((f) => f.name === 'autoconsent');
200+
201+
if (autoconsentFeature) {
202+
expect(
203+
autoconsentFeature.size,
204+
`Feature 'autoconsent' on ${platform} is ${autoconsentFeature.sizeKB}KB, ` +
205+
`exceeding 10% growth limit of ${allowedSizeKB.toFixed(2)}KB ` +
206+
`(base: ${baseSizeKB}KB for ${isAndroid ? 'Android' : 'other platforms'})`,
207+
).to.be.at.most(allowedSizeBytes);
208+
}
209+
});
210+
});
211+
212+
it('should track significant feature size increases', function () {
213+
if (!analysisData.maxSizes) {
214+
this.skip();
215+
return;
216+
}
217+
218+
const { maxSizes } = analysisData;
219+
const configFiles = getConfigFiles();
220+
const significantIncreases = [];
221+
222+
configFiles.forEach(({ filepath, filename }) => {
223+
const config = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
224+
const featureSizes = analyzeFeatureSizes(config);
225+
const platform = filename.replace('-config.json', '');
226+
227+
featureSizes.forEach((feature) => {
228+
const baselineSize = maxSizes[feature.name]?.size || 0;
229+
if (baselineSize > 0) {
230+
const growthPercent = ((feature.size - baselineSize) / baselineSize) * 100;
231+
232+
// Flag features that grew by more than 25%
233+
if (growthPercent > 25) {
234+
significantIncreases.push({
235+
feature: feature.name,
236+
platform,
237+
currentSize: feature.sizeKB,
238+
baselineSize: (baselineSize / 1024).toFixed(2),
239+
growthPercent: growthPercent.toFixed(1),
240+
});
241+
}
242+
}
243+
});
244+
});
245+
246+
if (significantIncreases.length > 0) {
247+
console.log('\n⚠️ Features with significant size increases (>25%):');
248+
significantIncreases.forEach(({ feature, platform, currentSize, baselineSize, growthPercent }) => {
249+
console.log(` ${feature} on ${platform}: ${baselineSize}KB → ${currentSize}KB (+${growthPercent}%)`);
250+
});
251+
console.log('');
252+
}
253+
254+
// Verify we processed the analysis
255+
expect(configFiles.length).to.be.greaterThan(0);
256+
});
257+
});
258+
});

tests/file-size-tests.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { expect } from 'chai';
4+
5+
/**
6+
* File size validation tests. These tests ensure the built config files stay within
7+
* reasonable size limits to prevent performance issues for clients.
8+
*/
9+
describe('File size validation', () => {
10+
const GENERATED_DIR = path.join(import.meta.dirname, '..', 'generated');
11+
const MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2MB hard cap
12+
13+
// Get all config versions and files
14+
const getConfigFiles = () => {
15+
const files = [];
16+
if (!fs.existsSync(GENERATED_DIR)) {
17+
throw new Error('Generated directory does not exist. Run `npm run build` first.');
18+
}
19+
20+
const versions = fs
21+
.readdirSync(GENERATED_DIR)
22+
.filter((dir) => dir.startsWith('v') && fs.statSync(path.join(GENERATED_DIR, dir)).isDirectory());
23+
24+
for (const version of versions) {
25+
const versionDir = path.join(GENERATED_DIR, version);
26+
const configFiles = fs.readdirSync(versionDir).filter((file) => file.endsWith('-config.json'));
27+
28+
for (const file of configFiles) {
29+
files.push({
30+
version,
31+
filename: file,
32+
filepath: path.join(versionDir, file),
33+
});
34+
}
35+
}
36+
37+
return files;
38+
};
39+
40+
describe('2MB hard cap enforcement', () => {
41+
const configFiles = getConfigFiles();
42+
43+
configFiles.forEach(({ version, filename, filepath }) => {
44+
it(`${version}/${filename} should not exceed 2MB`, () => {
45+
const stats = fs.statSync(filepath);
46+
const sizeInMB = (stats.size / (1024 * 1024)).toFixed(2);
47+
48+
expect(stats.size, `File ${version}/${filename} is ${sizeInMB}MB, exceeding 2MB limit`).to.be.at.most(MAX_FILE_SIZE_BYTES);
49+
});
50+
});
51+
52+
it('should have at least one config file to test', () => {
53+
expect(configFiles.length).to.be.greaterThan(0);
54+
});
55+
});
56+
57+
describe('File size reporting', () => {
58+
it('should report sizes of all config files', () => {
59+
const configFiles = getConfigFiles();
60+
61+
console.log('\n=== Config File Sizes ===');
62+
let totalSize = 0;
63+
64+
configFiles.forEach(({ version, filename, filepath }) => {
65+
const stats = fs.statSync(filepath);
66+
const sizeInKB = (stats.size / 1024).toFixed(1);
67+
const sizeInMB = (stats.size / (1024 * 1024)).toFixed(2);
68+
totalSize += stats.size;
69+
70+
console.log(`${version}/${filename}: ${sizeInKB}KB (${sizeInMB}MB)`);
71+
});
72+
73+
const totalSizeInMB = (totalSize / (1024 * 1024)).toFixed(2);
74+
console.log(`\nTotal size across all configs: ${totalSizeInMB}MB`);
75+
console.log('=========================\n');
76+
77+
// Verify we processed config files
78+
expect(configFiles.length).to.be.greaterThan(0);
79+
});
80+
});
81+
});

0 commit comments

Comments
 (0)