ai-evaluate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-test.log +51 -0
- package/README.md +420 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +4 -0
- package/coverage/evaluate.ts.html +574 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +146 -0
- package/coverage/index.ts.html +145 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/worker-template.ts.html +1948 -0
- package/dist/evaluate.d.ts +62 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +188 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/worker-template.d.ts +40 -0
- package/dist/worker-template.d.ts.map +1 -0
- package/dist/worker-template.js +3628 -0
- package/dist/worker-template.js.map +1 -0
- package/package.json +46 -0
- package/src/evaluate.ts +217 -0
- package/src/index.ts +21 -0
- package/src/types.ts +174 -0
- package/src/worker-template.ts +3677 -0
- package/test/evaluate-extended.test.ts +469 -0
- package/test/evaluate.test.ts +253 -0
- package/test/index.test.ts +95 -0
- package/test/worker-template.test.ts +430 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
> ai-sandbox@0.0.1 test /Users/nathanclevenger/projects/mdx.org.ai/primitives/packages/ai-sandbox
|
|
3
|
+
> vitest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
DEV v2.1.9 /Users/nathanclevenger/projects/mdx.org.ai/primitives/packages/ai-sandbox
|
|
7
|
+
|
|
8
|
+
✓ test/worker-template.test.ts (65 tests) 8ms
|
|
9
|
+
stdout | test/evaluate.test.ts > evaluate > script execution > captures console output
|
|
10
|
+
hello
|
|
11
|
+
|
|
12
|
+
stderr | test/evaluate.test.ts > evaluate > script execution > captures console output
|
|
13
|
+
warning
|
|
14
|
+
error
|
|
15
|
+
|
|
16
|
+
stdout | test/index.test.ts > types > LogEntry has correct shape
|
|
17
|
+
test
|
|
18
|
+
|
|
19
|
+
stderr | test/evaluate.test.ts > evaluate > script execution > handles script errors
|
|
20
|
+
Script error: test error
|
|
21
|
+
|
|
22
|
+
✓ test/index.test.ts (8 tests) 421ms
|
|
23
|
+
stderr | test/evaluate-extended.test.ts > evaluate - extended tests > module errors > captures module runtime errors
|
|
24
|
+
Module error: module error
|
|
25
|
+
|
|
26
|
+
stdout | test/evaluate-extended.test.ts > evaluate - extended tests > console methods > captures console.info
|
|
27
|
+
info message
|
|
28
|
+
|
|
29
|
+
stdout | test/evaluate-extended.test.ts > evaluate - extended tests > console methods > captures console.debug
|
|
30
|
+
debug message
|
|
31
|
+
|
|
32
|
+
stdout | test/evaluate-extended.test.ts > evaluate - extended tests > console methods > stringifies objects in console output
|
|
33
|
+
{ a: 1 }
|
|
34
|
+
|
|
35
|
+
stdout | test/evaluate-extended.test.ts > evaluate - extended tests > console methods > joins multiple console arguments
|
|
36
|
+
a b c
|
|
37
|
+
|
|
38
|
+
stderr | test/evaluate-extended.test.ts > evaluate - extended tests > async scripts > handles Promise rejection in script
|
|
39
|
+
Script error: async error
|
|
40
|
+
|
|
41
|
+
✓ test/evaluate.test.ts (18 tests) 765ms
|
|
42
|
+
✓ test/evaluate-extended.test.ts (40 tests) 1352ms
|
|
43
|
+
|
|
44
|
+
Test Files 4 passed (4)
|
|
45
|
+
Tests 131 passed (131)
|
|
46
|
+
Start at 14:14:37
|
|
47
|
+
Duration 1.87s (transform 186ms, setup 0ms, collect 405ms, tests 2.55s, environment 0ms, prepare 339ms)
|
|
48
|
+
|
|
49
|
+
PASS Waiting for file changes...
|
|
50
|
+
press h to show help, press q to quit
|
|
51
|
+
ELIFECYCLE Test failed. See above for more details.
|
package/README.md
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# ai-evaluate
|
|
2
|
+
|
|
3
|
+
Secure code execution in sandboxed environments. Run untrusted code safely using Cloudflare Workers or Miniflare.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add ai-evaluate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { evaluate } from 'ai-evaluate'
|
|
15
|
+
|
|
16
|
+
// Run a simple script
|
|
17
|
+
const result = await evaluate({
|
|
18
|
+
script: '1 + 1'
|
|
19
|
+
})
|
|
20
|
+
// { success: true, value: 2, logs: [], duration: 5 }
|
|
21
|
+
|
|
22
|
+
// With a module and tests
|
|
23
|
+
const result = await evaluate({
|
|
24
|
+
module: `
|
|
25
|
+
export const add = (a, b) => a + b
|
|
26
|
+
export const multiply = (a, b) => a * b
|
|
27
|
+
`,
|
|
28
|
+
tests: `
|
|
29
|
+
describe('math', () => {
|
|
30
|
+
it('adds numbers', () => {
|
|
31
|
+
expect(add(2, 3)).toBe(5);
|
|
32
|
+
})
|
|
33
|
+
it('multiplies numbers', () => {
|
|
34
|
+
expect(multiply(2, 3)).toBe(6);
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
`,
|
|
38
|
+
script: 'add(10, 20)'
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Secure isolation** - Code runs in a sandboxed V8 isolate
|
|
45
|
+
- **Vitest-compatible tests** - `describe`, `it`, `expect` in global scope
|
|
46
|
+
- **Module exports** - Define modules and use exports in scripts/tests
|
|
47
|
+
- **Cloudflare Workers** - Uses worker_loaders in production
|
|
48
|
+
- **Miniflare** - Uses Miniflare for local development and Node.js
|
|
49
|
+
- **Network isolation** - External network access blocked by default
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
### evaluate(options)
|
|
54
|
+
|
|
55
|
+
Execute code in a sandboxed environment.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
interface EvaluateOptions {
|
|
59
|
+
/** Module code with exports */
|
|
60
|
+
module?: string
|
|
61
|
+
/** Test code using vitest-style API */
|
|
62
|
+
tests?: string
|
|
63
|
+
/** Script code to run (module exports in scope) */
|
|
64
|
+
script?: string
|
|
65
|
+
/** Timeout in milliseconds (default: 5000) */
|
|
66
|
+
timeout?: number
|
|
67
|
+
/** Environment variables */
|
|
68
|
+
env?: Record<string, string>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface EvaluateResult {
|
|
72
|
+
/** Whether execution succeeded */
|
|
73
|
+
success: boolean
|
|
74
|
+
/** Return value from script */
|
|
75
|
+
value?: unknown
|
|
76
|
+
/** Console output */
|
|
77
|
+
logs: LogEntry[]
|
|
78
|
+
/** Test results (if tests provided) */
|
|
79
|
+
testResults?: TestResults
|
|
80
|
+
/** Error message if failed */
|
|
81
|
+
error?: string
|
|
82
|
+
/** Execution time in ms */
|
|
83
|
+
duration: number
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### createEvaluator(env)
|
|
88
|
+
|
|
89
|
+
Create an evaluate function bound to a specific environment. Useful for Cloudflare Workers.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { createEvaluator } from 'ai-evaluate'
|
|
93
|
+
|
|
94
|
+
export default {
|
|
95
|
+
async fetch(request, env) {
|
|
96
|
+
const sandbox = createEvaluator(env)
|
|
97
|
+
const result = await sandbox({
|
|
98
|
+
script: '1 + 1'
|
|
99
|
+
})
|
|
100
|
+
return Response.json(result)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Usage Patterns
|
|
106
|
+
|
|
107
|
+
### Simple Script Execution
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const result = await evaluate({
|
|
111
|
+
script: `
|
|
112
|
+
const x = 10;
|
|
113
|
+
const y = 20;
|
|
114
|
+
return x + y;
|
|
115
|
+
`
|
|
116
|
+
})
|
|
117
|
+
// result.value === 30
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Module with Exports
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const result = await evaluate({
|
|
124
|
+
module: `
|
|
125
|
+
exports.greet = (name) => \`Hello, \${name}!\`;
|
|
126
|
+
exports.sum = (...nums) => nums.reduce((a, b) => a + b, 0);
|
|
127
|
+
`,
|
|
128
|
+
script: `
|
|
129
|
+
console.log(greet('World'));
|
|
130
|
+
return sum(1, 2, 3, 4, 5);
|
|
131
|
+
`
|
|
132
|
+
})
|
|
133
|
+
// result.value === 15
|
|
134
|
+
// result.logs[0].message === 'Hello, World!'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Running Tests
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const result = await evaluate({
|
|
141
|
+
module: `
|
|
142
|
+
exports.isPrime = (n) => {
|
|
143
|
+
if (n < 2) return false;
|
|
144
|
+
for (let i = 2; i <= Math.sqrt(n); i++) {
|
|
145
|
+
if (n % i === 0) return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
};
|
|
149
|
+
`,
|
|
150
|
+
tests: `
|
|
151
|
+
describe('isPrime', () => {
|
|
152
|
+
it('returns false for numbers less than 2', () => {
|
|
153
|
+
expect(isPrime(0)).toBe(false);
|
|
154
|
+
expect(isPrime(1)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('returns true for prime numbers', () => {
|
|
158
|
+
expect(isPrime(2)).toBe(true);
|
|
159
|
+
expect(isPrime(3)).toBe(true);
|
|
160
|
+
expect(isPrime(17)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns false for composite numbers', () => {
|
|
164
|
+
expect(isPrime(4)).toBe(false);
|
|
165
|
+
expect(isPrime(9)).toBe(false);
|
|
166
|
+
expect(isPrime(100)).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
`
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
console.log(result.testResults)
|
|
173
|
+
// {
|
|
174
|
+
// total: 3,
|
|
175
|
+
// passed: 3,
|
|
176
|
+
// failed: 0,
|
|
177
|
+
// skipped: 0,
|
|
178
|
+
// tests: [...]
|
|
179
|
+
// }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Test Framework
|
|
183
|
+
|
|
184
|
+
The sandbox provides a vitest-compatible test API with async support.
|
|
185
|
+
|
|
186
|
+
### describe / it / test
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
describe('group name', () => {
|
|
190
|
+
it('test name', () => {
|
|
191
|
+
// test code
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('another test', () => {
|
|
195
|
+
// test code
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it.skip('skipped test', () => {
|
|
199
|
+
// won't run
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it.only('only this test', () => {
|
|
203
|
+
// when .only is used, only these tests run
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Async Tests
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
describe('async operations', () => {
|
|
212
|
+
it('supports async/await', async () => {
|
|
213
|
+
const result = await someAsyncFunction();
|
|
214
|
+
expect(result).toBe('expected');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('supports promises', () => {
|
|
218
|
+
return fetchData().then(data => {
|
|
219
|
+
expect(data).toBeDefined();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Hooks
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
describe('with setup', () => {
|
|
229
|
+
let data;
|
|
230
|
+
|
|
231
|
+
beforeEach(() => {
|
|
232
|
+
data = { count: 0 };
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
afterEach(() => {
|
|
236
|
+
data = null;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('uses setup data', () => {
|
|
240
|
+
data.count++;
|
|
241
|
+
expect(data.count).toBe(1);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### expect matchers
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// Equality
|
|
250
|
+
expect(value).toBe(expected) // Strict equality (===)
|
|
251
|
+
expect(value).toEqual(expected) // Deep equality
|
|
252
|
+
expect(value).toStrictEqual(expected) // Strict deep equality
|
|
253
|
+
|
|
254
|
+
// Truthiness
|
|
255
|
+
expect(value).toBeTruthy() // Truthy check
|
|
256
|
+
expect(value).toBeFalsy() // Falsy check
|
|
257
|
+
expect(value).toBeNull() // null check
|
|
258
|
+
expect(value).toBeUndefined() // undefined check
|
|
259
|
+
expect(value).toBeDefined() // not undefined
|
|
260
|
+
expect(value).toBeNaN() // NaN check
|
|
261
|
+
|
|
262
|
+
// Numbers
|
|
263
|
+
expect(value).toBeGreaterThan(n) // > comparison
|
|
264
|
+
expect(value).toBeLessThan(n) // < comparison
|
|
265
|
+
expect(value).toBeGreaterThanOrEqual(n)// >= comparison
|
|
266
|
+
expect(value).toBeLessThanOrEqual(n) // <= comparison
|
|
267
|
+
expect(value).toBeCloseTo(n, digits) // Floating point comparison
|
|
268
|
+
|
|
269
|
+
// Strings
|
|
270
|
+
expect(value).toMatch(/pattern/) // Regex match
|
|
271
|
+
expect(value).toMatch('substring') // Contains substring
|
|
272
|
+
|
|
273
|
+
// Arrays & Strings
|
|
274
|
+
expect(value).toContain(item) // Array/string contains
|
|
275
|
+
expect(value).toContainEqual(item) // Array contains (deep equality)
|
|
276
|
+
expect(value).toHaveLength(n) // Length check
|
|
277
|
+
|
|
278
|
+
// Objects
|
|
279
|
+
expect(value).toHaveProperty('path') // Has property
|
|
280
|
+
expect(value).toHaveProperty('path', v)// Has property with value
|
|
281
|
+
expect(value).toMatchObject(partial) // Partial object match
|
|
282
|
+
|
|
283
|
+
// Types
|
|
284
|
+
expect(value).toBeInstanceOf(Class) // instanceof check
|
|
285
|
+
expect(value).toBeTypeOf('string') // typeof check
|
|
286
|
+
|
|
287
|
+
// Errors
|
|
288
|
+
expect(fn).toThrow() // Throws any error
|
|
289
|
+
expect(fn).toThrow('message') // Throws with message
|
|
290
|
+
expect(fn).toThrow(/pattern/) // Throws matching pattern
|
|
291
|
+
expect(fn).toThrow(ErrorClass) // Throws specific error type
|
|
292
|
+
|
|
293
|
+
// Negated matchers
|
|
294
|
+
expect(value).not.toBe(expected)
|
|
295
|
+
expect(value).not.toEqual(expected)
|
|
296
|
+
expect(value).not.toContain(item)
|
|
297
|
+
expect(fn).not.toThrow()
|
|
298
|
+
|
|
299
|
+
// Promise matchers
|
|
300
|
+
await expect(promise).resolves.toBe(value)
|
|
301
|
+
await expect(promise).rejects.toThrow('error')
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Cloudflare Workers Setup
|
|
305
|
+
|
|
306
|
+
To use in Cloudflare Workers with worker_loaders:
|
|
307
|
+
|
|
308
|
+
### wrangler.toml
|
|
309
|
+
|
|
310
|
+
```toml
|
|
311
|
+
name = "my-worker"
|
|
312
|
+
main = "src/index.ts"
|
|
313
|
+
|
|
314
|
+
[[worker_loaders]]
|
|
315
|
+
binding = "LOADER"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Worker Code
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { createEvaluator } from 'ai-evaluate'
|
|
322
|
+
|
|
323
|
+
export interface Env {
|
|
324
|
+
LOADER: unknown
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default {
|
|
328
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
329
|
+
const sandbox = createEvaluator(env)
|
|
330
|
+
|
|
331
|
+
const { code, tests } = await request.json()
|
|
332
|
+
|
|
333
|
+
const result = await sandbox({
|
|
334
|
+
module: code,
|
|
335
|
+
tests: tests
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
return Response.json(result)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Node.js / Development
|
|
344
|
+
|
|
345
|
+
In Node.js or during development, the evaluate function automatically uses Miniflare:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { evaluate } from 'ai-evaluate'
|
|
349
|
+
|
|
350
|
+
// Miniflare is used automatically when LOADER binding is not present
|
|
351
|
+
const result = await evaluate({
|
|
352
|
+
script: 'return "Hello from Node!"'
|
|
353
|
+
})
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Make sure `miniflare` is installed:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
pnpm add miniflare
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Security
|
|
363
|
+
|
|
364
|
+
The sandbox provides several security features:
|
|
365
|
+
|
|
366
|
+
1. **V8 Isolate** - Code runs in an isolated V8 context
|
|
367
|
+
2. **No Network** - External network access is blocked (`globalOutbound: null`)
|
|
368
|
+
3. **No File System** - No access to the file system
|
|
369
|
+
4. **Memory Limits** - Standard Worker memory limits apply
|
|
370
|
+
5. **CPU Limits** - Execution time is limited
|
|
371
|
+
|
|
372
|
+
## Example: Code Evaluation API
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { evaluate } from 'ai-evaluate'
|
|
376
|
+
import { Hono } from 'hono'
|
|
377
|
+
|
|
378
|
+
const app = new Hono()
|
|
379
|
+
|
|
380
|
+
app.post('/evaluate', async (c) => {
|
|
381
|
+
const { module, tests, script } = await c.req.json()
|
|
382
|
+
|
|
383
|
+
const result = await evaluate({
|
|
384
|
+
module,
|
|
385
|
+
tests,
|
|
386
|
+
script,
|
|
387
|
+
timeout: 5000
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
return c.json(result)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
export default app
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Types
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
interface LogEntry {
|
|
400
|
+
level: 'log' | 'warn' | 'error' | 'info' | 'debug'
|
|
401
|
+
message: string
|
|
402
|
+
timestamp: number
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
interface TestResults {
|
|
406
|
+
total: number
|
|
407
|
+
passed: number
|
|
408
|
+
failed: number
|
|
409
|
+
skipped: number
|
|
410
|
+
tests: TestResult[]
|
|
411
|
+
duration: number
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface TestResult {
|
|
415
|
+
name: string
|
|
416
|
+
passed: boolean
|
|
417
|
+
error?: string
|
|
418
|
+
duration: number
|
|
419
|
+
}
|
|
420
|
+
```
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
body, html {
|
|
2
|
+
margin:0; padding: 0;
|
|
3
|
+
height: 100%;
|
|
4
|
+
}
|
|
5
|
+
body {
|
|
6
|
+
font-family: Helvetica Neue, Helvetica, Arial;
|
|
7
|
+
font-size: 14px;
|
|
8
|
+
color:#333;
|
|
9
|
+
}
|
|
10
|
+
.small { font-size: 12px; }
|
|
11
|
+
*, *:after, *:before {
|
|
12
|
+
-webkit-box-sizing:border-box;
|
|
13
|
+
-moz-box-sizing:border-box;
|
|
14
|
+
box-sizing:border-box;
|
|
15
|
+
}
|
|
16
|
+
h1 { font-size: 20px; margin: 0;}
|
|
17
|
+
h2 { font-size: 14px; }
|
|
18
|
+
pre {
|
|
19
|
+
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
-moz-tab-size: 2;
|
|
23
|
+
-o-tab-size: 2;
|
|
24
|
+
tab-size: 2;
|
|
25
|
+
}
|
|
26
|
+
a { color:#0074D9; text-decoration:none; }
|
|
27
|
+
a:hover { text-decoration:underline; }
|
|
28
|
+
.strong { font-weight: bold; }
|
|
29
|
+
.space-top1 { padding: 10px 0 0 0; }
|
|
30
|
+
.pad2y { padding: 20px 0; }
|
|
31
|
+
.pad1y { padding: 10px 0; }
|
|
32
|
+
.pad2x { padding: 0 20px; }
|
|
33
|
+
.pad2 { padding: 20px; }
|
|
34
|
+
.pad1 { padding: 10px; }
|
|
35
|
+
.space-left2 { padding-left:55px; }
|
|
36
|
+
.space-right2 { padding-right:20px; }
|
|
37
|
+
.center { text-align:center; }
|
|
38
|
+
.clearfix { display:block; }
|
|
39
|
+
.clearfix:after {
|
|
40
|
+
content:'';
|
|
41
|
+
display:block;
|
|
42
|
+
height:0;
|
|
43
|
+
clear:both;
|
|
44
|
+
visibility:hidden;
|
|
45
|
+
}
|
|
46
|
+
.fl { float: left; }
|
|
47
|
+
@media only screen and (max-width:640px) {
|
|
48
|
+
.col3 { width:100%; max-width:100%; }
|
|
49
|
+
.hide-mobile { display:none!important; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.quiet {
|
|
53
|
+
color: #7f7f7f;
|
|
54
|
+
color: rgba(0,0,0,0.5);
|
|
55
|
+
}
|
|
56
|
+
.quiet a { opacity: 0.7; }
|
|
57
|
+
|
|
58
|
+
.fraction {
|
|
59
|
+
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
60
|
+
font-size: 10px;
|
|
61
|
+
color: #555;
|
|
62
|
+
background: #E8E8E8;
|
|
63
|
+
padding: 4px 5px;
|
|
64
|
+
border-radius: 3px;
|
|
65
|
+
vertical-align: middle;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
div.path a:link, div.path a:visited { color: #333; }
|
|
69
|
+
table.coverage {
|
|
70
|
+
border-collapse: collapse;
|
|
71
|
+
margin: 10px 0 0 0;
|
|
72
|
+
padding: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
table.coverage td {
|
|
76
|
+
margin: 0;
|
|
77
|
+
padding: 0;
|
|
78
|
+
vertical-align: top;
|
|
79
|
+
}
|
|
80
|
+
table.coverage td.line-count {
|
|
81
|
+
text-align: right;
|
|
82
|
+
padding: 0 5px 0 20px;
|
|
83
|
+
}
|
|
84
|
+
table.coverage td.line-coverage {
|
|
85
|
+
text-align: right;
|
|
86
|
+
padding-right: 10px;
|
|
87
|
+
min-width:20px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
table.coverage td span.cline-any {
|
|
91
|
+
display: inline-block;
|
|
92
|
+
padding: 0 5px;
|
|
93
|
+
width: 100%;
|
|
94
|
+
}
|
|
95
|
+
.missing-if-branch {
|
|
96
|
+
display: inline-block;
|
|
97
|
+
margin-right: 5px;
|
|
98
|
+
border-radius: 3px;
|
|
99
|
+
position: relative;
|
|
100
|
+
padding: 0 4px;
|
|
101
|
+
background: #333;
|
|
102
|
+
color: yellow;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.skip-if-branch {
|
|
106
|
+
display: none;
|
|
107
|
+
margin-right: 10px;
|
|
108
|
+
position: relative;
|
|
109
|
+
padding: 0 4px;
|
|
110
|
+
background: #ccc;
|
|
111
|
+
color: white;
|
|
112
|
+
}
|
|
113
|
+
.missing-if-branch .typ, .skip-if-branch .typ {
|
|
114
|
+
color: inherit !important;
|
|
115
|
+
}
|
|
116
|
+
.coverage-summary {
|
|
117
|
+
border-collapse: collapse;
|
|
118
|
+
width: 100%;
|
|
119
|
+
}
|
|
120
|
+
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
|
121
|
+
.keyline-all { border: 1px solid #ddd; }
|
|
122
|
+
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
|
123
|
+
.coverage-summary tbody { border: 1px solid #bbb; }
|
|
124
|
+
.coverage-summary td { border-right: 1px solid #bbb; }
|
|
125
|
+
.coverage-summary td:last-child { border-right: none; }
|
|
126
|
+
.coverage-summary th {
|
|
127
|
+
text-align: left;
|
|
128
|
+
font-weight: normal;
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
}
|
|
131
|
+
.coverage-summary th.file { border-right: none !important; }
|
|
132
|
+
.coverage-summary th.pct { }
|
|
133
|
+
.coverage-summary th.pic,
|
|
134
|
+
.coverage-summary th.abs,
|
|
135
|
+
.coverage-summary td.pct,
|
|
136
|
+
.coverage-summary td.abs { text-align: right; }
|
|
137
|
+
.coverage-summary td.file { white-space: nowrap; }
|
|
138
|
+
.coverage-summary td.pic { min-width: 120px !important; }
|
|
139
|
+
.coverage-summary tfoot td { }
|
|
140
|
+
|
|
141
|
+
.coverage-summary .sorter {
|
|
142
|
+
height: 10px;
|
|
143
|
+
width: 7px;
|
|
144
|
+
display: inline-block;
|
|
145
|
+
margin-left: 0.5em;
|
|
146
|
+
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
|
147
|
+
}
|
|
148
|
+
.coverage-summary .sorted .sorter {
|
|
149
|
+
background-position: 0 -20px;
|
|
150
|
+
}
|
|
151
|
+
.coverage-summary .sorted-desc .sorter {
|
|
152
|
+
background-position: 0 -10px;
|
|
153
|
+
}
|
|
154
|
+
.status-line { height: 10px; }
|
|
155
|
+
/* yellow */
|
|
156
|
+
.cbranch-no { background: yellow !important; color: #111; }
|
|
157
|
+
/* dark red */
|
|
158
|
+
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
|
159
|
+
.low .chart { border:1px solid #C21F39 }
|
|
160
|
+
.highlighted,
|
|
161
|
+
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
|
162
|
+
background: #C21F39 !important;
|
|
163
|
+
}
|
|
164
|
+
/* medium red */
|
|
165
|
+
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
|
166
|
+
/* light red */
|
|
167
|
+
.low, .cline-no { background:#FCE1E5 }
|
|
168
|
+
/* light green */
|
|
169
|
+
.high, .cline-yes { background:rgb(230,245,208) }
|
|
170
|
+
/* medium green */
|
|
171
|
+
.cstat-yes { background:rgb(161,215,106) }
|
|
172
|
+
/* dark green */
|
|
173
|
+
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
|
174
|
+
.high .chart { border:1px solid rgb(77,146,33) }
|
|
175
|
+
/* dark yellow (gold) */
|
|
176
|
+
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
|
177
|
+
.medium .chart { border:1px solid #f9cd0b; }
|
|
178
|
+
/* light yellow */
|
|
179
|
+
.medium { background: #fff4c2; }
|
|
180
|
+
|
|
181
|
+
.cstat-skip { background: #ddd; color: #111; }
|
|
182
|
+
.fstat-skip { background: #ddd; color: #111 !important; }
|
|
183
|
+
.cbranch-skip { background: #ddd !important; color: #111; }
|
|
184
|
+
|
|
185
|
+
span.cline-neutral { background: #eaeaea; }
|
|
186
|
+
|
|
187
|
+
.coverage-summary td.empty {
|
|
188
|
+
opacity: .5;
|
|
189
|
+
padding-top: 4px;
|
|
190
|
+
padding-bottom: 4px;
|
|
191
|
+
line-height: 1;
|
|
192
|
+
color: #888;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.cover-fill, .cover-empty {
|
|
196
|
+
display:inline-block;
|
|
197
|
+
height: 12px;
|
|
198
|
+
}
|
|
199
|
+
.chart {
|
|
200
|
+
line-height: 0;
|
|
201
|
+
}
|
|
202
|
+
.cover-empty {
|
|
203
|
+
background: white;
|
|
204
|
+
}
|
|
205
|
+
.cover-full {
|
|
206
|
+
border-right: none !important;
|
|
207
|
+
}
|
|
208
|
+
pre.prettyprint {
|
|
209
|
+
border: none !important;
|
|
210
|
+
padding: 0 !important;
|
|
211
|
+
margin: 0 !important;
|
|
212
|
+
}
|
|
213
|
+
.com { color: #999 !important; }
|
|
214
|
+
.ignore-none { color: #999; font-weight: normal; }
|
|
215
|
+
|
|
216
|
+
.wrapper {
|
|
217
|
+
min-height: 100%;
|
|
218
|
+
height: auto !important;
|
|
219
|
+
height: 100%;
|
|
220
|
+
margin: 0 auto -48px;
|
|
221
|
+
}
|
|
222
|
+
.footer, .push {
|
|
223
|
+
height: 48px;
|
|
224
|
+
}
|