mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
feat: Use ranges with human readable sizes (#2589)
Replace separate minimum and maximum heap-size filters with range strings. Values can use byte counts or units, such as `1024-2048`, `1MB-2MB`, `-1MiB`, and `1GiB-`. A bare value such as `10M` is treated as a minimum. The range syntax is used by `query_heapsnapshot_objects` for retained and self sizes and by `get_heapsnapshot_edges` for retained sizes.
This commit is contained in:
@@ -546,9 +546,9 @@ in the DevTools Elements panel (if any).
|
||||
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
|
||||
- **nodeId** (number) **(required)**: The node ID to get outgoing edges for.
|
||||
- **excludePrimitives** (boolean) _(optional)_: Whether to exclude primitive target nodes. Default is true.
|
||||
- **minRetainedSize** (number) _(optional)_: Minimum retained size in bytes for target nodes.
|
||||
- **pageIdx** (number) _(optional)_: The page index for pagination.
|
||||
- **pageSize** (number) _(optional)_: The page size for pagination.
|
||||
- **retainedSize** (string) _(optional)_: Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-") for target nodes. A single value is treated as a minimum. Currently, only the lower bound is applied.
|
||||
- **sortBy** (enum: "retainedSize", "selfSize", "name") _(optional)_: Sort order for edges. Default is retainedSize.
|
||||
|
||||
---
|
||||
@@ -603,21 +603,19 @@ in the DevTools Elements panel (if any).
|
||||
|
||||
### `query_heapsnapshot_objects`
|
||||
|
||||
**Description:** Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)
|
||||
**Description:** Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, retainedSize, selfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
|
||||
- **className** (string) _(optional)_: Optional regex or text matching object class name.
|
||||
- **isDetached** (boolean) _(optional)_: Whether to filter for detached DOM nodes.
|
||||
- **maxRetainedSize** (number) _(optional)_: Maximum retained size in bytes.
|
||||
- **maxSelfSize** (number) _(optional)_: Maximum self size in bytes.
|
||||
- **minRetainedSize** (number) _(optional)_: Minimum retained size in bytes.
|
||||
- **minSelfSize** (number) _(optional)_: Minimum self size in bytes.
|
||||
- **nodeType** (string) _(optional)_: Optional V8 node type filter (e.g. object, closure, string, array, code).
|
||||
- **pageIdx** (number) _(optional)_: The page index for pagination.
|
||||
- **pageSize** (number) _(optional)_: The page size for pagination.
|
||||
- **propertyName** (string) _(optional)_: Optional property name filter for outgoing reference edges.
|
||||
- **retainedSize** (string) _(optional)_: Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum.
|
||||
- **selfSize** (string) _(optional)_: Inclusive self size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum.
|
||||
- **sortBy** (enum: "retainedSize", "selfSize", "id") _(optional)_: Sort order for results. Default is retainedSize.
|
||||
|
||||
---
|
||||
|
||||
+16
-25
@@ -518,10 +518,11 @@ export const commands: Commands = {
|
||||
required: false,
|
||||
enum: ['retainedSize', 'selfSize', 'name'],
|
||||
},
|
||||
minRetainedSize: {
|
||||
name: 'minRetainedSize',
|
||||
type: 'number',
|
||||
description: 'Minimum retained size in bytes for target nodes.',
|
||||
retainedSize: {
|
||||
name: 'retainedSize',
|
||||
type: 'string',
|
||||
description:
|
||||
'Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-") for target nodes. A single value is treated as a minimum. Currently, only the lower bound is applied.',
|
||||
required: false,
|
||||
},
|
||||
excludePrimitives: {
|
||||
@@ -1102,7 +1103,7 @@ export const commands: Commands = {
|
||||
},
|
||||
query_heapsnapshot_objects: {
|
||||
description:
|
||||
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)',
|
||||
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, retainedSize, selfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)',
|
||||
category: 'Memory',
|
||||
args: {
|
||||
filePath: {
|
||||
@@ -1131,28 +1132,18 @@ export const commands: Commands = {
|
||||
'Optional V8 node type filter (e.g. object, closure, string, array, code).',
|
||||
required: false,
|
||||
},
|
||||
minRetainedSize: {
|
||||
name: 'minRetainedSize',
|
||||
type: 'number',
|
||||
description: 'Minimum retained size in bytes.',
|
||||
retainedSize: {
|
||||
name: 'retainedSize',
|
||||
type: 'string',
|
||||
description:
|
||||
'Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum.',
|
||||
required: false,
|
||||
},
|
||||
maxRetainedSize: {
|
||||
name: 'maxRetainedSize',
|
||||
type: 'number',
|
||||
description: 'Maximum retained size in bytes.',
|
||||
required: false,
|
||||
},
|
||||
minSelfSize: {
|
||||
name: 'minSelfSize',
|
||||
type: 'number',
|
||||
description: 'Minimum self size in bytes.',
|
||||
required: false,
|
||||
},
|
||||
maxSelfSize: {
|
||||
name: 'maxSelfSize',
|
||||
type: 'number',
|
||||
description: 'Maximum self size in bytes.',
|
||||
selfSize: {
|
||||
name: 'selfSize',
|
||||
type: 'string',
|
||||
description:
|
||||
'Inclusive self size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum.',
|
||||
required: false,
|
||||
},
|
||||
isDetached: {
|
||||
|
||||
@@ -831,11 +831,16 @@
|
||||
},
|
||||
{
|
||||
"name": "min_retained_size",
|
||||
"argType": "number"
|
||||
"argType": "number",
|
||||
"isDeprecated": true
|
||||
},
|
||||
{
|
||||
"name": "exclude_primitives",
|
||||
"argType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "retained_size_length",
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1000,19 +1005,23 @@
|
||||
},
|
||||
{
|
||||
"name": "min_retained_size",
|
||||
"argType": "number"
|
||||
"argType": "number",
|
||||
"isDeprecated": true
|
||||
},
|
||||
{
|
||||
"name": "max_retained_size",
|
||||
"argType": "number"
|
||||
"argType": "number",
|
||||
"isDeprecated": true
|
||||
},
|
||||
{
|
||||
"name": "min_self_size",
|
||||
"argType": "number"
|
||||
"argType": "number",
|
||||
"isDeprecated": true
|
||||
},
|
||||
{
|
||||
"name": "max_self_size",
|
||||
"argType": "number"
|
||||
"argType": "number",
|
||||
"isDeprecated": true
|
||||
},
|
||||
{
|
||||
"name": "is_detached",
|
||||
@@ -1029,6 +1038,14 @@
|
||||
{
|
||||
"name": "page_size",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "retained_size_length",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "self_size_length",
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+17
-26
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import {zod} from '../third_party/index.js';
|
||||
import {byteSizeRangeSchema} from '../utils/bytes.js';
|
||||
|
||||
import {ToolCategory} from './categories.js';
|
||||
import {definePageTool, defineTool} from './ToolDefinition.js';
|
||||
@@ -305,10 +306,9 @@ export const getHeapSnapshotEdges = defineTool({
|
||||
.enum(['retainedSize', 'selfSize', 'name'])
|
||||
.optional()
|
||||
.describe('Sort order for edges. Default is retainedSize.'),
|
||||
minRetainedSize: zod
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Minimum retained size in bytes for target nodes.'),
|
||||
retainedSize: byteSizeRangeSchema(
|
||||
'Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-") for target nodes. A single value is treated as a minimum. Currently, only the lower bound is applied.',
|
||||
).optional(),
|
||||
excludePrimitives: zod
|
||||
.boolean()
|
||||
.optional()
|
||||
@@ -322,7 +322,8 @@ export const getHeapSnapshotEdges = defineTool({
|
||||
request.params.nodeId,
|
||||
{
|
||||
sortBy: request.params.sortBy ?? 'retainedSize',
|
||||
minRetainedSize: request.params.minRetainedSize,
|
||||
// DevTools currently only supports a lower retained-size bound here.
|
||||
minRetainedSize: request.params.retainedSize?.min,
|
||||
excludePrimitives: request.params.excludePrimitives ?? true,
|
||||
},
|
||||
);
|
||||
@@ -469,7 +470,7 @@ export const getHeapSnapshotObjectDetails = defineTool({
|
||||
export const queryHeapSnapshotObjects = defineTool({
|
||||
name: 'query_heapsnapshot_objects',
|
||||
description:
|
||||
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy).',
|
||||
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, retainedSize, selfSize, isDetached, sortBy).',
|
||||
annotations: {
|
||||
category: ToolCategory.MEMORY,
|
||||
readOnlyHint: true,
|
||||
@@ -493,22 +494,12 @@ export const queryHeapSnapshotObjects = defineTool({
|
||||
.describe(
|
||||
'Optional V8 node type filter (e.g. object, closure, string, array, code).',
|
||||
),
|
||||
minRetainedSize: zod
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Minimum retained size in bytes.'),
|
||||
maxRetainedSize: zod
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Maximum retained size in bytes.'),
|
||||
minSelfSize: zod
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Minimum self size in bytes.'),
|
||||
maxSelfSize: zod
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Maximum self size in bytes.'),
|
||||
retainedSize: byteSizeRangeSchema(
|
||||
'Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum.',
|
||||
).optional(),
|
||||
selfSize: byteSizeRangeSchema(
|
||||
'Inclusive self size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum.',
|
||||
).optional(),
|
||||
isDetached: zod
|
||||
.boolean()
|
||||
.optional()
|
||||
@@ -527,10 +518,10 @@ export const queryHeapSnapshotObjects = defineTool({
|
||||
className: request.params.className,
|
||||
propertyName: request.params.propertyName,
|
||||
nodeType: request.params.nodeType,
|
||||
minRetainedSize: request.params.minRetainedSize,
|
||||
maxRetainedSize: request.params.maxRetainedSize,
|
||||
minSelfSize: request.params.minSelfSize,
|
||||
maxSelfSize: request.params.maxSelfSize,
|
||||
minRetainedSize: request.params.retainedSize?.min,
|
||||
maxRetainedSize: request.params.retainedSize?.max,
|
||||
minSelfSize: request.params.selfSize?.min,
|
||||
maxSelfSize: request.params.selfSize?.max,
|
||||
isDetached: request.params.isDetached,
|
||||
sortBy: request.params.sortBy,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {zod} from '../third_party/index.js';
|
||||
|
||||
const BYTE_UNITS: Readonly<Record<string, number>> = {
|
||||
b: 1,
|
||||
byte: 1,
|
||||
bytes: 1,
|
||||
k: 1000,
|
||||
kb: 1000,
|
||||
kib: 1024,
|
||||
m: 1000 * 1000,
|
||||
mb: 1000 * 1000,
|
||||
mib: 1024 * 1024,
|
||||
g: 1000 * 1000 * 1000,
|
||||
gb: 1000 * 1000 * 1000,
|
||||
gib: 1024 * 1024 * 1024,
|
||||
t: 1000 * 1000 * 1000 * 1000,
|
||||
tb: 1000 * 1000 * 1000 * 1000,
|
||||
tib: 1024 * 1024 * 1024 * 1024,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a byte size string (e.g. "1M", "1MB", "500KB", "1.5GB", "1024") into bytes.
|
||||
*/
|
||||
export function parseByteSize(value: string): number {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === '') {
|
||||
throw new Error(`Invalid byte size: "${value}"`);
|
||||
}
|
||||
|
||||
const match = trimmed.match(/^(\d+(?:\.\d+)?)\s*([a-zA-Z]+)?$/);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid byte size format: "${value}". Expected a number or format like "1024", "1M", "1MB", "1G", "1GB".`,
|
||||
);
|
||||
}
|
||||
|
||||
const numMatch = match[1];
|
||||
if (numMatch === undefined) {
|
||||
throw new Error(`Invalid byte size: "${value}"`);
|
||||
}
|
||||
const num = Number(numMatch);
|
||||
if (!Number.isFinite(num) || num < 0) {
|
||||
throw new Error(`Invalid byte size: "${value}"`);
|
||||
}
|
||||
|
||||
const unitMatch = match[2];
|
||||
if (unitMatch === undefined) {
|
||||
return Math.round(num);
|
||||
}
|
||||
|
||||
const unit = unitMatch.toLowerCase();
|
||||
const multiplier = BYTE_UNITS[unit];
|
||||
if (multiplier === undefined) {
|
||||
throw new Error(
|
||||
`Unknown unit "${unitMatch}" in "${value}". Supported units: B, KB, KiB, MB, MiB, GB, GiB, TB, TiB.`,
|
||||
);
|
||||
}
|
||||
|
||||
const bytes = Math.round(num * multiplier);
|
||||
if (!Number.isFinite(bytes)) {
|
||||
throw new Error(`Invalid byte size: "${value}"`);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export interface ByteSizeRange {
|
||||
min: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an inclusive byte-size range (e.g. "1MB", "1MB-2MB", "-1MB", "1MB-").
|
||||
*/
|
||||
export function parseByteSizeRange(value: string): ByteSizeRange {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.includes('-')) {
|
||||
return {min: parseByteSize(trimmed), max: undefined};
|
||||
}
|
||||
|
||||
const parts = trimmed.split('-');
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(
|
||||
`Invalid byte size range: "${value}". Expected a size or range like "1MB", "1MB-2MB", "-1MB", or "1MB-".`,
|
||||
);
|
||||
}
|
||||
|
||||
const minValue = parts[0];
|
||||
const maxValue = parts[1];
|
||||
if (minValue === undefined || maxValue === undefined) {
|
||||
throw new Error(`Invalid byte size range: "${value}"`);
|
||||
}
|
||||
|
||||
const minText = minValue.trim();
|
||||
const maxText = maxValue.trim();
|
||||
if (minText === '' && maxText === '') {
|
||||
throw new Error(
|
||||
`Invalid byte size range: "${value}". At least one bound is required.`,
|
||||
);
|
||||
}
|
||||
|
||||
const min = minText === '' ? 0 : parseByteSize(minText);
|
||||
const max = maxText === '' ? undefined : parseByteSize(maxText);
|
||||
if (max !== undefined && min > max) {
|
||||
throw new Error(
|
||||
`Invalid byte size range: "${value}". The lower bound must not exceed the upper bound.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {min, max};
|
||||
}
|
||||
|
||||
export function byteSizeRangeSchema(description: string) {
|
||||
return zod
|
||||
.string()
|
||||
.transform((value, context) => {
|
||||
try {
|
||||
return parseByteSizeRange(value);
|
||||
} catch (error) {
|
||||
context.addIssue({
|
||||
code: zod.ZodIssueCode.custom,
|
||||
message:
|
||||
error instanceof Error ? error.message : 'Invalid byte size range',
|
||||
});
|
||||
return zod.NEVER;
|
||||
}
|
||||
})
|
||||
.describe(description);
|
||||
}
|
||||
@@ -302,6 +302,15 @@ Showing 1-2 of 56 (Page 1 of 28).
|
||||
Next page: 1
|
||||
`;
|
||||
|
||||
exports[`memory > get_heapsnapshot_edges > with retainedSize range 1`] = `
|
||||
## Heap Snapshot Data
|
||||
name,type,nodeId,nodeName,selfSize,retainedSize
|
||||
map,internal,25577,system / Map,0.0 kB,0.7 kB
|
||||
constructor,property,25575,String,0.1 kB,0.4 kB
|
||||
__proto__,property,25329,{constructor, __defineGetter__, __defineSetter__, …, get __proto__, set __proto__, toLocaleString},0.1 kB,0.3 kB
|
||||
Showing 1-3 of 3 (Page 1 of 1).
|
||||
`;
|
||||
|
||||
exports[`memory > get_heapsnapshot_edges > with valid nodeId 1`] = `
|
||||
## Heap Snapshot Data
|
||||
name,type,nodeId,nodeName,selfSize,retainedSize
|
||||
@@ -450,6 +459,23 @@ Not retained by context size: 798 kB (11792 objects)
|
||||
Total size: 802 kB
|
||||
`;
|
||||
|
||||
exports[`memory > query_heapsnapshot_objects > with an unbounded retainedSize filter 1`] = `
|
||||
## Heap Snapshot Data
|
||||
nodeId,nodeName,type,distance,selfSize,retainedSize
|
||||
1,,synthetic,100000000,0.0 kB,802 kB
|
||||
7249,system / NativeContext / https://example.com,hidden,1,1.2 kB,350 kB
|
||||
7199,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7307,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7195,system / NativeContext / https://example.com,hidden,1,1.2 kB,60.1 kB
|
||||
27635,Window (global*) / https://example.com,object,2,40.5 kB,51.0 kB
|
||||
30887,Window (global*) / https://example.com,object,2,40.5 kB,50.9 kB
|
||||
3,(GC roots),synthetic,100000001,0.0 kB,45.6 kB
|
||||
49547,,array,2,4.1 kB,41.0 kB
|
||||
36241,,array,2,4.1 kB,41.0 kB
|
||||
Showing 1-10 of 148 (Page 1 of 15).
|
||||
Next page: 1
|
||||
`;
|
||||
|
||||
exports[`memory > query_heapsnapshot_objects > with className filter 1`] = `
|
||||
## Heap Snapshot Data
|
||||
nodeId,nodeName,type,distance,selfSize,retainedSize
|
||||
@@ -484,23 +510,6 @@ Showing 1-10 of 27466 (Page 1 of 2747).
|
||||
Next page: 1
|
||||
`;
|
||||
|
||||
exports[`memory > query_heapsnapshot_objects > with minRetainedSize filter 1`] = `
|
||||
## Heap Snapshot Data
|
||||
nodeId,nodeName,type,distance,selfSize,retainedSize
|
||||
1,,synthetic,100000000,0.0 kB,802 kB
|
||||
7249,system / NativeContext / https://example.com,hidden,1,1.2 kB,350 kB
|
||||
7199,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7307,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7195,system / NativeContext / https://example.com,hidden,1,1.2 kB,60.1 kB
|
||||
27635,Window (global*) / https://example.com,object,2,40.5 kB,51.0 kB
|
||||
30887,Window (global*) / https://example.com,object,2,40.5 kB,50.9 kB
|
||||
3,(GC roots),synthetic,100000001,0.0 kB,45.6 kB
|
||||
49547,,array,2,4.1 kB,41.0 kB
|
||||
36241,,array,2,4.1 kB,41.0 kB
|
||||
Showing 1-10 of 148 (Page 1 of 15).
|
||||
Next page: 1
|
||||
`;
|
||||
|
||||
exports[`memory > query_heapsnapshot_objects > with sortBy selfSize and pagination 1`] = `
|
||||
## Heap Snapshot Data
|
||||
nodeId,nodeName,type,distance,selfSize,retainedSize
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getHeapSnapshotObjectDetails,
|
||||
queryHeapSnapshotObjects,
|
||||
} from '../../src/tools/memory.js';
|
||||
import {parseByteSizeRange} from '../../src/utils/bytes.js';
|
||||
import {stableIdSymbol} from '../../src/utils/id.js';
|
||||
import {resolveCanonicalPath} from '../../src/utils/files.js';
|
||||
import {withMcpContext} from '../utils.js';
|
||||
@@ -447,6 +448,34 @@ describe('memory', () => {
|
||||
t.assert.snapshot(output);
|
||||
});
|
||||
});
|
||||
|
||||
it('with retainedSize range', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
const filePath = join(
|
||||
process.cwd(),
|
||||
'tests/fixtures/example.heapsnapshot',
|
||||
);
|
||||
|
||||
await getHeapSnapshotEdges.handler(
|
||||
{
|
||||
params: {
|
||||
filePath,
|
||||
nodeId: 25341,
|
||||
retainedSize: parseByteSizeRange('100B-100B'),
|
||||
},
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
const responseData = await response.handle(context);
|
||||
const output = responseData.content
|
||||
.map(c => (c.type === 'text' ? c.text : ''))
|
||||
.join('\n');
|
||||
|
||||
t.assert.snapshot(output);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_heapsnapshot_dominators', () => {
|
||||
@@ -697,7 +726,7 @@ describe('memory', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('with minRetainedSize filter', async t => {
|
||||
it('with an unbounded retainedSize filter', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
const filePath = join(
|
||||
process.cwd(),
|
||||
@@ -705,7 +734,13 @@ describe('memory', () => {
|
||||
);
|
||||
|
||||
await queryHeapSnapshotObjects.handler(
|
||||
{params: {filePath, minRetainedSize: 1000, pageSize: 10}},
|
||||
{
|
||||
params: {
|
||||
filePath,
|
||||
retainedSize: parseByteSizeRange('1KB'),
|
||||
pageSize: 10,
|
||||
},
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import {describe, it} from 'node:test';
|
||||
|
||||
import {
|
||||
byteSizeRangeSchema,
|
||||
parseByteSize,
|
||||
parseByteSizeRange,
|
||||
} from '../../src/utils/bytes.js';
|
||||
|
||||
describe('parseByteSize', () => {
|
||||
it('should parse plain numeric strings', () => {
|
||||
assert.strictEqual(parseByteSize('0'), 0);
|
||||
assert.strictEqual(parseByteSize('1024'), 1024);
|
||||
assert.strictEqual(parseByteSize(' 1048576 '), 1048576);
|
||||
});
|
||||
|
||||
it('should parse bytes units', () => {
|
||||
assert.strictEqual(parseByteSize('100B'), 100);
|
||||
assert.strictEqual(parseByteSize('100b'), 100);
|
||||
assert.strictEqual(parseByteSize('100 Bytes'), 100);
|
||||
assert.strictEqual(parseByteSize('100 byte'), 100);
|
||||
});
|
||||
|
||||
it('should parse kilobytes units', () => {
|
||||
assert.strictEqual(parseByteSize('1K'), 1000);
|
||||
assert.strictEqual(parseByteSize('1k'), 1000);
|
||||
assert.strictEqual(parseByteSize('1KB'), 1000);
|
||||
assert.strictEqual(parseByteSize('1kb'), 1000);
|
||||
assert.strictEqual(parseByteSize('1KiB'), 1024);
|
||||
assert.strictEqual(parseByteSize('1.5 KB'), 1500);
|
||||
});
|
||||
|
||||
it('should parse megabytes units', () => {
|
||||
assert.strictEqual(parseByteSize('1M'), 1000000);
|
||||
assert.strictEqual(parseByteSize('1m'), 1000000);
|
||||
assert.strictEqual(parseByteSize('1MB'), 1000000);
|
||||
assert.strictEqual(parseByteSize('1mb'), 1000000);
|
||||
assert.strictEqual(parseByteSize('1MiB'), 1048576);
|
||||
assert.strictEqual(parseByteSize('2.5MB'), 2500000);
|
||||
});
|
||||
|
||||
it('should parse gigabytes units', () => {
|
||||
assert.strictEqual(parseByteSize('1G'), 1000000000);
|
||||
assert.strictEqual(parseByteSize('1g'), 1000000000);
|
||||
assert.strictEqual(parseByteSize('1GB'), 1000000000);
|
||||
assert.strictEqual(parseByteSize('1gb'), 1000000000);
|
||||
assert.strictEqual(parseByteSize('1GiB'), 1073741824);
|
||||
assert.strictEqual(parseByteSize('0.5GB'), 500000000);
|
||||
});
|
||||
|
||||
it('should parse terabytes units', () => {
|
||||
assert.strictEqual(parseByteSize('1T'), 1000000000000);
|
||||
assert.strictEqual(parseByteSize('1TB'), 1000000000000);
|
||||
assert.strictEqual(parseByteSize('1TiB'), 1099511627776);
|
||||
});
|
||||
|
||||
it('should throw for invalid inputs', () => {
|
||||
assert.throws(() => parseByteSize(''), /Invalid byte size/);
|
||||
assert.throws(() => parseByteSize(' '), /Invalid byte size/);
|
||||
assert.throws(() => parseByteSize('abc'), /Invalid byte size/);
|
||||
assert.throws(() => parseByteSize('10XYZ'), /Unknown unit/);
|
||||
assert.throws(() => parseByteSize('-10MB'), /Invalid byte size/);
|
||||
assert.throws(() => parseByteSize(`${'9'.repeat(300)}TB`), /Invalid byte/);
|
||||
});
|
||||
|
||||
it('should parse inclusive byte-size ranges', () => {
|
||||
assert.deepStrictEqual(parseByteSizeRange('10M'), {
|
||||
min: 10 * 1000 * 1000,
|
||||
max: undefined,
|
||||
});
|
||||
assert.deepStrictEqual(parseByteSizeRange('1KB-2MiB'), {
|
||||
min: 1000,
|
||||
max: 2 * 1024 * 1024,
|
||||
});
|
||||
assert.deepStrictEqual(parseByteSizeRange('-1MB'), {
|
||||
min: 0,
|
||||
max: 1000 * 1000,
|
||||
});
|
||||
assert.deepStrictEqual(parseByteSizeRange('1MiB-'), {
|
||||
min: 1024 * 1024,
|
||||
max: undefined,
|
||||
});
|
||||
assert.deepStrictEqual(parseByteSizeRange(' 1 KB - 2 MB '), {
|
||||
min: 1000,
|
||||
max: 2 * 1000 * 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject invalid byte-size ranges', () => {
|
||||
assert.throws(() => parseByteSizeRange(''), /Invalid byte size/);
|
||||
assert.throws(() => parseByteSizeRange('-'), /At least one bound/);
|
||||
assert.throws(() => parseByteSizeRange('2MB-1MB'), /lower bound/);
|
||||
assert.throws(() => parseByteSizeRange('1MB-2MB-3MB'), /Invalid byte/);
|
||||
assert.throws(() => parseByteSizeRange('invalid-1MB'), /Invalid byte/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('byteSizeRangeSchema', () => {
|
||||
it('parses valid byte-size range string', () => {
|
||||
const schema = byteSizeRangeSchema('test');
|
||||
const result = schema.safeParse('1KB-2MB');
|
||||
assert.strictEqual(result.success, true);
|
||||
if (result.success) {
|
||||
assert.deepStrictEqual(result.data, {
|
||||
min: 1000,
|
||||
max: 2 * 1000 * 1000,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('fails for invalid byte-size range string', () => {
|
||||
const schema = byteSizeRangeSchema('test');
|
||||
const result = schema.safeParse('2MB-1MB');
|
||||
assert.strictEqual(result.success, false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user