-
-
Notifications
You must be signed in to change notification settings - Fork 182
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
346 additions
and
0 deletions.
There are no files selected for viewing
43 changes: 43 additions & 0 deletions
43
website/blog/2024-12-01-advent-of-pbt-day-1/AdventOfTheDay.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import adventBuggy from './buggy.mjs'; | ||
import { buildAdventOfTheDay } from './AdventOfTheDayBuilder'; | ||
|
||
const { AdventPlaygroundOfTheDay, FormOfTheDay } = buildAdventOfTheDay({ | ||
day: 1, | ||
buildBuggyAdvent: adventBuggy, | ||
referenceAdvent: sortLetters, | ||
parser, | ||
placeholderForm: 'Anne=15\nPaul=38\nElena=81', | ||
functionName: 'sortLetters', | ||
signature: 'function sortLetters(letters: Letter[]): Letter[];', | ||
signatureExtras: ['type Letter = { name: string; age: number };'], | ||
}); | ||
|
||
export { AdventPlaygroundOfTheDay, FormOfTheDay }; | ||
|
||
// Reference implementation | ||
|
||
type Letter = { name: string; age: number }; | ||
|
||
function sortLetters(letters: Letter[]): Letter[] { | ||
const clonedLetters = [...letters]; | ||
return clonedLetters.sort((la, lb) => | ||
la.age !== lb.age ? la.age - lb.age : la.name > lb.name ? 1 : la.name < lb.name ? -1 : 0, | ||
); | ||
} | ||
|
||
// Inputs parser | ||
|
||
const lineRegex = /^([a-z]+)=(\d+)$/; | ||
function parser(answer: string): unknown[] | undefined { | ||
const parsedAnswer: Letter[] = []; | ||
for (const line of answer.trim().split('\n')) { | ||
const m = lineRegex.exec(line); | ||
if (m === null) { | ||
throw new Error( | ||
'Each line of the answer should follow the pattern: name=age, with name only made of characters in a-z and of length minimum one and age being exclusively between 7 and 77', | ||
); | ||
} | ||
parsedAnswer.push({ name: m[1], age: Number(m[2]) }); | ||
} | ||
return [parsedAnswer]; | ||
} |
147 changes: 147 additions & 0 deletions
147
website/blog/2024-12-01-advent-of-pbt-day-1/AdventOfTheDayBuilder.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import React, { useState } from 'react'; | ||
import { isEqual, set } from 'lodash'; | ||
import Admonition from '@theme/Admonition'; | ||
import AdventPlayground from './AdventPlayground'; | ||
|
||
const answerFieldName = 'answer'; | ||
|
||
type Options = { | ||
day: number; | ||
buildBuggyAdvent: () => (...args: unknown[]) => unknown; | ||
referenceAdvent: (...args: unknown[]) => unknown; | ||
postAdvent?: (adventOutput: unknown) => unknown; | ||
parser: (answer: string) => unknown[] | undefined; | ||
placeholderForm: string; | ||
functionName: string; | ||
signature: string; | ||
signatureExtras?: string[]; | ||
}; | ||
|
||
export function buildAdventOfTheDay(options: Options) { | ||
const { | ||
day, | ||
buildBuggyAdvent, | ||
referenceAdvent, | ||
postAdvent = (v) => v, | ||
parser, | ||
placeholderForm, | ||
functionName, | ||
signature, | ||
signatureExtras, | ||
} = options; | ||
|
||
const snippetLinesWithHeadingSpaces = String(buildBuggyAdvent).split('\n').slice(1, -1); | ||
const spacesCountToDrop = /^( +)/.exec(snippetLinesWithHeadingSpaces[0])[1].length; | ||
const spacesToDrop = ' '.repeat(spacesCountToDrop); | ||
const snippet = snippetLinesWithHeadingSpaces | ||
.map((line) => (line.startsWith(spacesToDrop) ? line.substring(spacesToDrop.length) : line)) | ||
.map((line) => line.replace(/^return /, 'export default ')) | ||
.join('\n'); | ||
|
||
function AdventPlaygroundOfTheDay() { | ||
return ( | ||
<AdventPlayground | ||
functionName={functionName} | ||
signature={signature} | ||
signatureExtras={signatureExtras} | ||
snippet={snippet} | ||
day={day} | ||
/> | ||
); | ||
} | ||
|
||
let lastError = null; | ||
const storageKey = `aopbt24-day${day}`; | ||
|
||
function retrievePastAnswerIfSolved(): string | null { | ||
try { | ||
const pastAnswer = localStorage.getItem(storageKey); | ||
if (pastAnswer === null) { | ||
return null; | ||
} | ||
const inputs = parser(pastAnswer); | ||
if (inputs === undefined) { | ||
return null; | ||
} | ||
const buggyAdvent = buildBuggyAdvent(); | ||
if (isEqual(postAdvent(buggyAdvent(...inputs)), postAdvent(referenceAdvent(...inputs)))) { | ||
return null; | ||
} | ||
return pastAnswer; | ||
} catch (err) { | ||
return null; | ||
} | ||
} | ||
|
||
function onSubmit(event: React.SyntheticEvent<HTMLFormElement>) { | ||
event.preventDefault(); | ||
try { | ||
const answer = extractAnswerFromForm(event); | ||
const inputs = parser(answer); | ||
if (inputs === undefined) { | ||
lastError = 'Malformed inputs provided!'; | ||
return; | ||
} | ||
const buggyAdvent = buildBuggyAdvent(); | ||
if (isEqual(postAdvent(buggyAdvent(...inputs)), postAdvent(referenceAdvent(...inputs)))) { | ||
lastError = 'The input you provided seems to be working well: Santa is looking for a bug!'; | ||
return; | ||
} | ||
lastError = null; | ||
localStorage.setItem(storageKey, answer); | ||
} catch (err) { | ||
lastError = `Malformed inputs provided!\n${(err as Error).message}`; | ||
} | ||
} | ||
|
||
function FormOfTheDay() { | ||
const [, setId] = useState(Symbol()); | ||
const pastAnswer = lastError === null ? retrievePastAnswerIfSolved() : null; | ||
|
||
return ( | ||
<> | ||
{pastAnswer !== null && ( | ||
<Admonition type="tip" icon="🎉" title="Congratulations"> | ||
<p>You solved this puzzle!</p> | ||
</Admonition> | ||
)} | ||
{lastError !== null && ( | ||
<Admonition type="danger"> | ||
<p>{lastError}</p> | ||
</Admonition> | ||
)} | ||
<form | ||
onSubmit={(e) => { | ||
onSubmit(e); | ||
setId(Symbol()); | ||
}} | ||
> | ||
<textarea | ||
name={answerFieldName} | ||
style={{ width: '100%', ...(pastAnswer !== null ? { backgroundColor: 'lightgreen' } : {}) }} | ||
rows={5} | ||
defaultValue={pastAnswer ?? undefined} | ||
placeholder={`Example of answer:\n${placeholderForm}`} | ||
></textarea> | ||
<br /> | ||
<button type="submit">Submit</button> | ||
</form> | ||
</> | ||
); | ||
} | ||
|
||
return { AdventPlaygroundOfTheDay, FormOfTheDay }; | ||
} | ||
|
||
function extractAnswerFromForm(event: React.SyntheticEvent<HTMLFormElement>): string { | ||
const form = event.currentTarget; | ||
const formElements = form.elements; | ||
if (!(answerFieldName in formElements)) { | ||
throw new Error(`No ${JSON.stringify(answerFieldName)} field attached to the form`); | ||
} | ||
const answer = (formElements[answerFieldName] as { value?: unknown }).value; | ||
if (typeof answer !== 'string') { | ||
throw new Error(`${JSON.stringify(answerFieldName)} field attached to the form must be of type string`); | ||
} | ||
return answer; | ||
} |
96 changes: 96 additions & 0 deletions
96
website/blog/2024-12-01-advent-of-pbt-day-1/AdventPlayground.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import React from 'react'; | ||
import { | ||
SandpackProvider, | ||
SandpackLayout, | ||
SandpackCodeEditor, | ||
SandpackTests, | ||
UnstyledOpenInCodeSandboxButton, | ||
} from '@codesandbox/sandpack-react'; | ||
import { atomDark } from '@codesandbox/sandpack-themes'; | ||
import Admonition from '@theme/Admonition'; | ||
|
||
type Props = { | ||
functionName: string; | ||
signature: string; | ||
signatureExtras?: string[]; | ||
snippet: string; | ||
day: number; | ||
}; | ||
|
||
export default function AdventPlayground(props: Props) { | ||
const { functionName, signature, signatureExtras, snippet, day } = props; | ||
const styleCodeEditor = { height: 400 }; | ||
const styleTests = { height: 200 }; | ||
|
||
const adventSpecLines = [ | ||
`import fc from 'fast-check';`, | ||
`import ${functionName} from './advent.js';`, | ||
``, | ||
...signatureExtras.map((extra) => `// declare ${extra}`), | ||
`// declare ${signature}`, | ||
`test('helping Santa', () => {`, | ||
` fc.assert(fc.property(fc.constant('noop'), (noop) => {`, | ||
` }));`, | ||
`})`, | ||
]; | ||
|
||
return ( | ||
<> | ||
<SandpackProvider | ||
theme={atomDark} | ||
files={{ | ||
[`/advent.js`]: { | ||
code: snippet, | ||
readOnly: true, | ||
active: false, | ||
hidden: true, | ||
}, | ||
[`/advent.spec.ts`]: { | ||
code: adventSpecLines.join('\n'), | ||
readOnly: false, | ||
active: true, | ||
hidden: false, | ||
}, | ||
'package.json': { | ||
code: JSON.stringify({ main: `src/advent.js` }), | ||
readOnly: true, | ||
active: false, | ||
hidden: true, | ||
}, | ||
}} | ||
customSetup={{ | ||
entry: `/advent.js`, | ||
dependencies: { | ||
'fast-check': 'latest', | ||
}, | ||
}} | ||
> | ||
<SandpackLayout> | ||
<SandpackCodeEditor style={styleCodeEditor} /> | ||
</SandpackLayout> | ||
<SandpackLayout> | ||
<SandpackTests verbose style={styleTests} /> | ||
</SandpackLayout> | ||
<p> | ||
<UnstyledOpenInCodeSandboxButton> | ||
Open in CodeSandbox for more options: including typings... | ||
</UnstyledOpenInCodeSandboxButton> | ||
</p> | ||
</SandpackProvider> | ||
<Admonition type="note"> | ||
<p> | ||
Can’t access the online playground? Prefer to run it locally? | ||
<br /> | ||
No problem! You can download the source file{' '} | ||
<a | ||
download={`advent-day-${String(day).padStart(2, '0')}.mjs`} | ||
href={`data:application/octet-stream;base64,${btoa(snippet)}`} | ||
> | ||
here | ||
</a> | ||
. | ||
</p> | ||
</Admonition> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// @ts-check | ||
|
||
export default function advent() { | ||
/** @typedef {{name: string; age: number;}} Letter */ | ||
|
||
/** | ||
* @param {Letter[]} letters | ||
* @returns {Letter[]} | ||
*/ | ||
return function sortLetters(letters) { | ||
const clonedLetters = [...letters]; | ||
return clonedLetters.sort((la, lb) => la.age - lb.age || la.name.codePointAt(0) - lb.name.codePointAt(0)); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
--- | ||
title: Advent of PBT 2024 · Day 1 | ||
authors: [dubzzz] | ||
tags: [advent-of-pbt, advent-of-pbt-2024] | ||
--- | ||
|
||
import {AdventPlaygroundOfTheDay,FormOfTheDay} from './AdventOfTheDay'; | ||
|
||
Christmas is at risk! In their rush to meet tight deadlines, Santa’s elves accidentally introduced bugs into critical algorithms. If these issues aren’t discovered in time, Christmas could be delayed for everyone worldwide! | ||
|
||
Your mission is to troubleshoot these black-box algorithms using the power of fast-check. | ||
|
||
The clock is ticking. Santa just pinged you with your first challenge: he’s struggling to answer children in the proper and efficient order. Something seems to be going wrong—can you uncover the issue and save Christmas? 🎄🔧 | ||
|
||
<!--truncate--> | ||
|
||
## Letters to Santa | ||
|
||
Each year, Santa receives billions of letters from children and adults all over the globe, each with their own unique wish lists and messages. To ensure timely responses before the big day, Santa has spent years refining his process. This year, his carefully designed system is entering its final stage. | ||
|
||
Santa has decided to prioritize his answers based on the following criteria: | ||
|
||
1. The younger the sender, the faster the answer. | ||
2. If two senders are the same age, sort their letters alphabetically by name using the `<` operator. | ||
|
||
To implement this system, Santa asked his elves for help. He instructed them that the input would be an array of letters, each represented as a string in the format `name=age`. The name would consist only of lowercase letters (`a-z`), with at least one character, and the age would be a number between 7 and 77. The elves’ task was to return a sorted version of this array based on the specified criteria. | ||
|
||
## Hands on | ||
|
||
The elves completed the task, but Santa is worried they may have made mistakes. | ||
|
||
Using the property-based testing features provided by fast-check, your mission is to uncover a set of inputs (letters) that break the elves’ implementation. | ||
|
||
Santa has entrusted you with the code created by the elves (though you can’t see it directly). You’re his last hope — can you find the flaws and save Christmas? 🎄🔧 | ||
|
||
<AdventPlaygroundOfTheDay /> | ||
|
||
## Your answer | ||
|
||
<FormOfTheDay /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters