Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
blank_issues_enabled: false
35 changes: 35 additions & 0 deletions .github/ISSUE_TEMPLATE/correction.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Correct an answer
description: An answer is wrong, outdated for a newer JDK, or misleading
title: "[correction] "
labels: ["correction"]
body:
- type: input
id: question
attributes:
label: Which question
description: The question id, or its number in the README.
placeholder: volatile-guarantees, or 23
validations:
required: true

- type: textarea
id: problem
attributes:
label: What is wrong
description: What the answer says, and what is actually true.
validations:
required: true

- type: textarea
id: source
attributes:
label: What settles it
description: A link to the JDK source, a JEP, the javadoc or the JLS is worth more than an argument.
validations:
required: true

- type: input
id: jdk
attributes:
label: JDK version, if it depends on one
placeholder: "changed in JDK 24 (JEP 491)"
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: gradle
directory: /
schedule:
interval: weekly
labels: [dependencies, build]
groups:
junit:
patterns: ["org.junit*"]

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
labels: [dependencies, build]
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ on:
pull_request:
branches: [ "master" ]

permissions:
contents: read

# A new push to a pull request makes the run for the previous push pointless.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: links

# Most answers link to runnable examples in java-concurrency-patterns. If that repository moves a
# file, the link here breaks silently, so check them on every change and once a week regardless.
on:
push:
branches: [ "master" ]
paths: [ "**.md" ]
pull_request:
branches: [ "master" ]
paths: [ "**.md" ]
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
links:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Check links
uses: lycheeverse/lychee-action@v2
with:
args: --no-progress --max-retries 3 './**/*.md'
fail: true
token: ${{ secrets.GITHUB_TOKEN }}
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ loud: the mechanism, the trade-off, and the follow-up the interviewer asks next.

It is also a quiz you can run. One file, no dependencies, ASCII only.

<!-- BEGIN SCREEN -->
```
+----------------------------------------------------------------------------------------+
| J A V A C O N C U R R E N C Y I N T E R V I E W |
| |
| writer |--- write x=1 ---[ release ]--------------------> |
| \ happens-before |
| reader -----------------[ acquire ]--- reads x == 1 ----> |
| |
| 166 questions across 16 topics, each answered with the reasoning, |
| not just the keyword an interviewer is listening for |
| |
| J A V A C O N C U R R E N C Y I N T E R V I E W |
| |
| writer --- write x = 1 --->[ release ] |
| | |
| | happens-before |
| v |
| reader ------------------->[ acquire ]--- reads x == 1 ---> |
| |
| 166 questions across 16 topics, each answered with the reasoning, |
| not just the keyword an interviewer is listening for |
| |
+----------------------------------------------------------------------------------------+

main menu
Expand All @@ -36,6 +41,7 @@ It is also a quiz you can run. One file, no dependencies, ASCII only.

choose >
```
<!-- END SCREEN -->

## Run it

Expand Down
136 changes: 97 additions & 39 deletions src/main/java/org/alxkm/interview/Quiz.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,57 @@ public final class Quiz {
private Quiz() {
}

// ------------------------------------------------------------- screens
//
// Plain strings rather than Ui calls, because the README shows the same screen and generates it
// from here. A hand-copied screenshot drifts by a column the first time anyone edits it.

private static final String MENU_PROMPT = "choose >";

/** A box whose sides line up regardless of what is in it. */
static List<String> boxLines(List<String> lines) {
int inner = WIDTH - 6;
List<String> out = new ArrayList<>();
out.add(" +" + "-".repeat(inner + 2) + "+");
for (String line : lines) {
String clipped = line.length() > inner ? line.substring(0, inner) : line;
out.add(" | " + clipped + " ".repeat(inner - clipped.length()) + " |");
}
out.add(" +" + "-".repeat(inner + 2) + "+");
return out;
}

/** The release and the acquire share a column, so the edge between them reads as one arrow. */
static List<String> bannerLines(int questions, int topics) {
return List.of(
"",
" J A V A C O N C U R R E N C Y I N T E R V I E W",
"",
" writer --- write x = 1 --->[ release ]",
" |",
" | happens-before",
" v",
" reader ------------------->[ acquire ]--- reads x == 1 --->",
"",
" " + questions + " questions across " + topics + " topics, each answered with the"
+ " reasoning,",
" not just the keyword an interviewer is listening for",
"");
}

static List<String> menuLines(String filter) {
return List.of(
" [1] quiz one topic",
" [2] mock interview 12 mixed questions, hardest topics first",
" [3] random ten",
" [4] flashcards no options, recall it yourself",
" [5] read a topic questions and answers, no marking",
" [6] review my misses the ones you got wrong before",
" [7] progress",
" [8] level filter currently: " + filter,
" [q] quit");
}

// ---------------------------------------------------------------- model

/** Difficulty as an interviewer would pitch it, not a judgement about the reader. */
Expand Down Expand Up @@ -472,15 +523,8 @@ String cyan(String text) {
return paint(CYAN, text);
}

/** A box whose sides line up regardless of what is in it. */
void box(List<String> lines) {
int inner = WIDTH - 6;
System.out.println(" +" + "-".repeat(inner + 2) + "+");
for (String line : lines) {
String clipped = line.length() > inner ? line.substring(0, inner) : line;
System.out.println(" | " + clipped + " ".repeat(inner - clipped.length()) + " |");
}
System.out.println(" +" + "-".repeat(inner + 2) + "+");
boxLines(lines).forEach(System.out::println);
}

/** The header every screen starts with: where you are on the left, how far along on the right. */
Expand All @@ -500,16 +544,7 @@ String bar(int done, int total, int width) {

void banner(int questions, int topics) {
blank();
box(List.of(
" J A V A C O N C U R R E N C Y I N T E R V I E W",
"",
" writer |--- write x=1 ---[ release ]-------------------->",
" \\ happens-before",
" reader -----------------[ acquire ]--- reads x == 1 ---->",
"",
" " + questions + " questions across " + topics + " topics, each answered with the"
+ " reasoning,",
" not just the keyword an interviewer is listening for"));
box(bannerLines(questions, topics));
blank();
}

Expand Down Expand Up @@ -679,18 +714,9 @@ void menu() {
while (true) {
ui.line(" " + ui.bold("main menu"));
ui.blank();
ui.line(" [1] quiz one topic");
ui.line(" [2] mock interview 12 mixed questions, hardest topics first");
ui.line(" [3] random ten");
ui.line(" [4] flashcards no options, recall it yourself");
ui.line(" [5] read a topic questions and answers, no marking");
ui.line(" [6] review my misses the ones you got wrong before");
ui.line(" [7] progress");
ui.line(" [8] level filter currently: "
+ (filter == null ? "all" : filter.label()));
ui.line(" [q] quit");
menuLines(filter == null ? "all" : filter.label()).forEach(ui::line);
ui.blank();
String choice = ui.ask("choose >").toLowerCase(Locale.ROOT);
String choice = ui.ask(MENU_PROMPT).toLowerCase(Locale.ROOT);
ui.blank();
switch (choice) {
case "1" -> quizTopic();
Expand Down Expand Up @@ -1095,22 +1121,54 @@ private static String escapeHtml(String text) {
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}

private static final String BEGIN_SCREEN = "<!-- BEGIN SCREEN -->";
private static final String END_SCREEN = "<!-- END SCREEN -->";

/** The main menu as a first-time reader sees it, for the top of the README. */
static String screen(Bank bank) {
List<String> lines = new ArrayList<>(boxLines(bannerLines(bank.questions().size(),
bank.topics().size())));
lines.add("");
lines.add(" main menu");
lines.add("");
lines.addAll(menuLines("all"));
lines.add("");
lines.add(" " + MENU_PROMPT);
return BEGIN_SCREEN + "\n```\n" + String.join("\n", lines) + "\n```\n" + END_SCREEN;
}

/**
* README.md with every generated part brought up to date: the screen, the catalogue, and the
* question count in the badge and the first sentence. The count sits in a line of badges, where a
* marker comment would break the paragraph, so it is matched by pattern instead.
*/
static String render(String readme, Bank bank) {
String body = replaceBetween(readme, BEGIN_SCREEN, END_SCREEN, screen(bank));
body = replaceBetween(body, BEGIN, END, catalogue(bank));
int count = bank.questions().size();
return body
.replaceAll("badge/questions-\\d+-", "badge/questions-" + count + "-")
.replaceAll("(?m)^\\d+ interview questions on Java concurrency",
count + " interview questions on Java concurrency");
}

private static String replaceBetween(String text, String begin, String end, String generated) {
int from = text.indexOf(begin);
int to = text.indexOf(end);
if (from < 0 || to < from) {
throw new IllegalStateException("README.md is missing the " + begin + " / " + end
+ " markers");
}
return text.substring(0, from) + generated + text.substring(to + end.length());
}

static void exportReadme(Bank bank) {
Path readme = questionsDir().getParent().resolve("README.md");
String generated = catalogue(bank);
String body;
try {
String existing = Files.isRegularFile(readme)
? Files.readString(readme, StandardCharsets.UTF_8)
: "";
int begin = existing.indexOf(BEGIN);
int end = existing.indexOf(END);
if (begin < 0 || end < 0) {
throw new IllegalStateException(
"README.md is missing the " + BEGIN + " / " + END + " markers");
}
body = existing.substring(0, begin) + generated + existing.substring(end + END.length());
Files.writeString(readme, body, StandardCharsets.UTF_8);
Files.writeString(readme, render(existing, bank), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException("cannot write " + readme, e);
}
Expand Down
8 changes: 3 additions & 5 deletions src/test/java/org/alxkm/interview/QuestionContentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,9 @@ void optionsAreComparableInLength(Quiz.Question question) {
void readmeIsUpToDate() throws IOException {
Path readme = Quiz.questionsDir().getParent().resolve("README.md");
String content = Files.readString(readme, StandardCharsets.UTF_8);
String expected = Quiz.catalogue(BANK);
int begin = content.indexOf("<!-- BEGIN QUESTIONS -->");
int end = content.indexOf("<!-- END QUESTIONS -->");
assertTrue(begin >= 0 && end > begin, "README.md has lost its generated section markers");
assertEquals(expected, content.substring(begin, end + "<!-- END QUESTIONS -->".length()),
// render() covers the screen, the catalogue and the question count, and throws if a marker
// has gone missing.
assertEquals(Quiz.render(content, BANK), content,
"README.md is out of date. Run ./gradlew readme");
}
}
Loading