mirror of https://github.com/01-edu/public.git
Browse Source
nb: subject could ask to print new value in the file after rm or add of an elem. For now I didn't put it because it's already a lot. TO be discussedpull/761/head
2 changed files with 486 additions and 0 deletions
@ -0,0 +1,375 @@ |
|||||||
|
import * as cp from 'child_process' |
||||||
|
import fs from 'fs/promises' |
||||||
|
import { join, resolve, isAbsolute } from 'path' |
||||||
|
import { tmpdir } from 'os' |
||||||
|
import { promisify } from 'util' |
||||||
|
const mkdir = fs.mkdir |
||||||
|
const rmdir = fs.rmdir |
||||||
|
const readFile = fs.readFile |
||||||
|
|
||||||
|
const exec = promisify(cp.exec) |
||||||
|
|
||||||
|
export const tests = [] |
||||||
|
const name = 'personal-shopper' |
||||||
|
const getRandomElem = () => |
||||||
|
Math.random() |
||||||
|
.toString(36) |
||||||
|
.substring(7) |
||||||
|
export const setup = async () => { |
||||||
|
const dir = tmpdir() |
||||||
|
const elems = {} |
||||||
|
const randomElem = getRandomElem() |
||||||
|
const anotherElem = getRandomElem() |
||||||
|
const commands = ['add', 'rm', 'delete', 'create', 'ls', 'help'] |
||||||
|
// check if already exists and rm
|
||||||
|
const exists = await fs |
||||||
|
.stat(`${dir}/${name}`) |
||||||
|
.catch((err) => (err.code === 'ENOENT' ? 'file not found: good.' : err)) |
||||||
|
if (Object.keys(exists).length) { |
||||||
|
await rmdir(`${dir}/${name}`, { recursive: true }) |
||||||
|
} |
||||||
|
|
||||||
|
await mkdir(`${dir}/${name}`) |
||||||
|
|
||||||
|
return { tmpPath: `${dir}/${name}`, elems, commands, randomElem, anotherElem } |
||||||
|
} |
||||||
|
const assistInShopping = async ({ |
||||||
|
file, |
||||||
|
keyword, |
||||||
|
elem, |
||||||
|
number, |
||||||
|
ctx, |
||||||
|
path, |
||||||
|
eq, |
||||||
|
}) => { |
||||||
|
// check if file exists before testing create - if exists, rm it
|
||||||
|
if (keyword === 'create') { |
||||||
|
const out = await fs |
||||||
|
.stat(`${ctx.tmpPath}/${file}`) |
||||||
|
.catch((err) => |
||||||
|
err.code === 'ENOENT' ? 'output file not found: normal' : err, |
||||||
|
) |
||||||
|
if (out.birthtime) await fs.rm(`${ctx.tmpPath}/${file}`) |
||||||
|
} |
||||||
|
|
||||||
|
const scriptPath = join(resolve(), path) |
||||||
|
const { stdout, stderr } = await exec( |
||||||
|
`node ${scriptPath} ${file} ${keyword} ${elem || ''} ${number || ''}`, |
||||||
|
{ |
||||||
|
cwd: ctx.tmpPath, |
||||||
|
}, |
||||||
|
) |
||||||
|
const out = await readFile(`${ctx.tmpPath}/${file}`, 'utf8').catch((err) => |
||||||
|
err.code === 'ENOENT' ? 'output file not found' : err, |
||||||
|
) |
||||||
|
|
||||||
|
if (keyword === 'create') return eq(out, '{}') |
||||||
|
|
||||||
|
if (keyword === 'add' || keyword === 'rm') { |
||||||
|
if (!elem) return eq(stderr.trim(), 'No elem specified.') |
||||||
|
if (number && isNaN(Number(number)) && keyword === 'rm') { |
||||||
|
return eq(stderr.trim(), 'Unexpected request: nothing has been removed.') |
||||||
|
} |
||||||
|
const content = JSON.parse(out) |
||||||
|
|
||||||
|
const addValue = !number || isNaN(Number(number)) ? 1 : number |
||||||
|
const rmValue = !number ? ctx.elems[elem] : number |
||||||
|
const newNumber = |
||||||
|
keyword === 'add' |
||||||
|
? (ctx.elems[elem] || 0) + addValue |
||||||
|
: (ctx.elems[elem] || 0) - rmValue |
||||||
|
const state = newNumber > 0 ? content[elem] === newNumber : !content[elem] |
||||||
|
ctx.elems[elem] = newNumber > 0 ? newNumber : 0 |
||||||
|
return state |
||||||
|
} |
||||||
|
|
||||||
|
if (keyword === 'ls') { |
||||||
|
const content = JSON.parse(out) |
||||||
|
const entries = Object.entries(content) |
||||||
|
return entries.length |
||||||
|
? eq(entries.map(([k, v]) => `- ${k} (${v})`).join('\n'), stdout.trim()) |
||||||
|
: eq(stdout.trim(), 'Empty list.') |
||||||
|
} |
||||||
|
|
||||||
|
if (keyword === 'help' || !keyword) { |
||||||
|
return ctx.commands.every((c) => stdout.includes(c)) |
||||||
|
} |
||||||
|
|
||||||
|
if (keyword === 'delete') return eq(out, 'output file not found') |
||||||
|
} |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'create', |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// no elem specifiel, should log an error
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'add', |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// add NaN value, should add 1
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'add', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: 'someNaN', |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// add without number, should add 1
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'add', |
||||||
|
elem: ctx.randomElem, |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// add random value, between 5 and 15
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'add', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: Math.floor(Math.random() * (15 - 5) + 5), |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// add negative value, bigger than current value - should delete the entry
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'add', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: -Math.floor(Math.random() * (25 - 16) + 16), |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// add value to prepare next test
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'add', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: Math.floor(Math.random() * (25 - 16) + 16), |
||||||
|
}) |
||||||
|
}) |
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// add negative exact nm of elem -> 0, should delete the entry
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'add', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: -ctx.elems[ctx.randomElem], |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// no elem specified, should log an error
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'rm', |
||||||
|
// no elem, no number
|
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// rm elem which is not in the list, should do nothing
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'rm', |
||||||
|
elem: ctx.randomElem, |
||||||
|
// no number
|
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// rm negative value, should add it
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'rm', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: -Math.floor(Math.random() * (25 - 16) + 16), |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// rm positive value, smaller than current value -> should substract it from current value
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'rm', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: Math.floor(Math.random() * (10 - 5) + 5), |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// should print the list with the elem as expected inside
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'ls', |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// rm without number, should delete the entry
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'rm', |
||||||
|
elem: ctx.randomElem, |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// should print an empty list
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'ls', |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
// add 2 elems - tests: ls several values, rm more or same value as current
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// add value to prepare next test
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'add', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: Math.floor(Math.random() * (25 - 16) + 16), |
||||||
|
}) |
||||||
|
}) |
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// add value to prepare next test
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'add', |
||||||
|
elem: ctx.anotherElem, |
||||||
|
number: ctx.elems[ctx.randomElem], // add same value than randomelem
|
||||||
|
}) |
||||||
|
}) |
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// test ls with several elems
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'ls', |
||||||
|
}) |
||||||
|
}) |
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// test where rm exat nm of elem -> 0, delete the entry
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'rm', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: ctx.elems[ctx.randomElem], |
||||||
|
}) |
||||||
|
}) |
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// test where rm more than nm of elem -> <0, delete the entry
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'rm', |
||||||
|
elem: ctx.randomElem, |
||||||
|
number: 2000, |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// should print the helper
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'help', |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
// should print the helper
|
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: '', |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
tests.push(async ({ path, eq, ctx }) => { |
||||||
|
return assistInShopping({ |
||||||
|
path, |
||||||
|
eq, |
||||||
|
ctx, |
||||||
|
file: `${name}.json`, |
||||||
|
keyword: 'delete', |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
Object.freeze(tests) |
@ -0,0 +1,111 @@ |
|||||||
|
## personal-shopper |
||||||
|
|
||||||
|
### Instructions |
||||||
|
|
||||||
|
You know your guests, but don't forget you have to feed them if you want to be considered as a good host. |
||||||
|
|
||||||
|
Create a `personal-shopper.mjs` script that: |
||||||
|
- Takes a file as first argument, for example `shopping-list.json` |
||||||
|
- Takes one of these keywords as second argument: |
||||||
|
- `create`: create the file |
||||||
|
- `delete`: delete the file |
||||||
|
- `add`: add a new element to the list in the file. |
||||||
|
- This command line must take a third argument which is the name of the new entry in your list. If no third argument is passed, console must print this error: `No elem specified.`. |
||||||
|
- This command line could take a fourth argument which is the number of elements you want for this new entry. |
||||||
|
- If there is no 4rth argument or the 4rth argument is `NaN`, 1 would be the value by default. |
||||||
|
- If the entry already exists, it would add 1 or more to the original value. |
||||||
|
- Using a negative number must behave as `rm` command. |
||||||
|
- `rm`: remove an element from the list in the file |
||||||
|
- This command line must take a third argument which is the name of the entry to remove from the list. |
||||||
|
- If no third argument is passed, console must print this error: `No elem specified.`. |
||||||
|
- If the entry does not exists, it does nothing. |
||||||
|
- This command line could take a fourth argument which is the number of elements you want to delete from this entry. |
||||||
|
- If there is no 4rth argument: it remove the entry. |
||||||
|
- If the 4rth argument is `NaN`, nothing is removed and console must print this error `Unexpected request: nothing has been removed`. |
||||||
|
- If the 4rth argument is a number, it will substract this number from the original value (if the new value is <= 0, it will remove the entry). |
||||||
|
- Using a negative number must behave as `add` command. |
||||||
|
- `help`: print all the command lines available, with a description of it (specifications in the examples) |
||||||
|
- `ls` or no more arguments: print the list in the console. |
||||||
|
- Each line is formated like this: `- element (number)` |
||||||
|
- If the list is empty, this message should appear in console: `Empty list.`. |
||||||
|
|
||||||
|
#### Examples |
||||||
|
|
||||||
|
- `node personal-shopper.mjs shopping-list.json create` would create the file |
||||||
|
- `node personal-shopper.mjs shopping-list.json delete` would remove the file |
||||||
|
|
||||||
|
- `node personal-shopper.mjs shopping-list.json add "tzatziki pot"` would update the content of the file like: |
||||||
|
```json |
||||||
|
'{ |
||||||
|
"tzatziki pot": 1 |
||||||
|
}' |
||||||
|
``` |
||||||
|
|
||||||
|
- `node personal-shopper.mjs shopping-list.json add carrots 5` would update the content of the file like: |
||||||
|
```json |
||||||
|
'{ |
||||||
|
"tzatziki pot": 1, |
||||||
|
"carrots": 5 |
||||||
|
}' |
||||||
|
``` |
||||||
|
|
||||||
|
- `node personal-shopper.mjs shopping-list.json add carrots 2` would update the content of the file like: |
||||||
|
```json |
||||||
|
'{ |
||||||
|
"tzatziki pot": 1, |
||||||
|
"carrots": 7 |
||||||
|
}' |
||||||
|
``` |
||||||
|
|
||||||
|
- `node personal-shopper.mjs shopping-list.json rm carrots 4` would update the content of the file like: |
||||||
|
```json |
||||||
|
'{ |
||||||
|
"tzatziki pot": 1, |
||||||
|
"carrots": 3 |
||||||
|
}' |
||||||
|
``` |
||||||
|
|
||||||
|
- `node personal-shopper.mjs shopping-list.json rm carrots` would update the content of the file like: |
||||||
|
```json |
||||||
|
'{ |
||||||
|
"tzatziki pot": 1 |
||||||
|
}' |
||||||
|
``` |
||||||
|
|
||||||
|
- `node personal-shopper.mjs shopping-list.json rm carrots -3` would update the content of the file like: |
||||||
|
```json |
||||||
|
'{ |
||||||
|
"tzatziki pot": 1, |
||||||
|
"carrots": 3 |
||||||
|
}' |
||||||
|
``` |
||||||
|
|
||||||
|
- `node personal-shopper.mjs shopping-list.json add carrots -3` would update the content of the file like: |
||||||
|
```json |
||||||
|
'{ |
||||||
|
"tzatziki pot": 1 |
||||||
|
}' |
||||||
|
``` |
||||||
|
|
||||||
|
- `node personal-shopper.mjs shopping-list.json ls` would print the list in your console like this: |
||||||
|
``` |
||||||
|
- tzatziki pot (1) |
||||||
|
``` |
||||||
|
|
||||||
|
- `node personal-shopper.mjs help` would print a list of all available commands in your console : |
||||||
|
``` |
||||||
|
Commands: |
||||||
|
- create: takes a filename as argument and create it (should have `.json` extension specified) |
||||||
|
- delete: takes a filename as argument and delete it |
||||||
|
<!-- etc. --> |
||||||
|
``` |
||||||
|
|
||||||
|
### Notions |
||||||
|
|
||||||
|
- [Node file system: `rm`](https://nodejs.org/docs/latest/api/fs.html#fs_fspromises_rm_path_options) |
||||||
|
- [Node file system: `writeFile`](https://nodejs.org/docs/latest/api/fs.html#fs_fspromises_writefile_file_data_options) |
||||||
|
- [`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) |
||||||
|
- [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) |
||||||
|
- [`isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN) |
||||||
|
- [`Number()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) |
||||||
|
- [`console.error()`](https://developer.mozilla.org/en-US/docs/Web/API/Console/error) |
Loading…
Reference in new issue