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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ L' F B2 L F2 B2 L' F2 L2 R B2 U' B' D' L2 D' F2 B2 R2 Fw2 U Rw2 Uw2 R' U' Rw2 R
```
![Image of a WCA 3x3 Scramble](docs/resources/images/three.svg)

#### Piping Scrambles to Draw

You can directly pipe the output of the `scramble` command to the `draw` command. This is useful for quickly visualizing a freshly generated scramble. When providing the scramble via standard input (stdin) like this, you can omit the `-s` or `--scramble` option for the `draw` command.

```bash
tnoodle scramble -p three | tnoodle draw -p three > output.svg
```
This command will generate a scramble for the 3x3x3 cube (`three`), send that scramble directly to the `draw` command (which also expects a 3x3x3 cube scramble), and save the resulting SVG image to `output.svg`.

## Legal
***Disclaimer:** This CLI tool is an **unofficial** project currently unaffiliated with the WCA. Scrambles generated by this tool are NOT authorized for use in any official WCA event. All such scrambles must be generated using the [official TNoodle program](https://www.worldcubeassociation.org/regulations/scrambles/).*

Expand Down
44 changes: 41 additions & 3 deletions src/main/java/tnoodlecli/commands/DrawCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;

import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
Expand Down Expand Up @@ -47,7 +49,7 @@ public class DrawCommand implements Runnable {
@Parameter(
names = { "-s", "--scramble" },
description = "The scramble to draw.",
required = true
required = false
)
private String scramble;

Expand All @@ -56,6 +58,40 @@ public class DrawCommand implements Runnable {

public void run() {
try {
if (scramble == null || scramble.isEmpty()) {
try {
if (System.in.available() > 0) {
Scanner scanner = new Scanner(System.in);
if (scanner.hasNextLine()) {
scramble = scanner.nextLine();
if (scramble.trim().isEmpty()) {
System.err.println("Error: Received empty scramble from standard input.");
System.exit(1);
}
} else {
// This case means stdin had available bytes but nothing was read by nextLine (e.g. only EOF)
System.err.println("Error: Standard input was open but no scramble string was provided.");
System.exit(1);
}
// We don't close the scanner for System.in here
} else {
// System.in.available() <= 0 means no input from stdin when -s is not used
System.err.println("Error: No scramble provided via -s/--scramble and no input detected on standard input.");
System.exit(1);
}
} catch (IOException e) {
// Handle IOException from System.in.available()
System.err.println("Error accessing standard input: " + e.getMessage());
System.exit(1);
}
}

// This check is now more of a safeguard, as the logic above should handle most empty/null cases.
if (scramble == null || scramble.trim().isEmpty()) {
System.err.println("Error: Scramble string not provided or is empty.");
System.exit(1);
}

Svg svg = PuzzleRegistry.valueOf(puzzle.toUpperCase()).getScrambler().drawScramble(scramble, null);
if (output == null) {
System.out.println(svg.toString());
Expand All @@ -64,11 +100,13 @@ public void run() {
try (PrintWriter writer = new PrintWriter(outputFile)){
writer.print(svg.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
e.printStackTrace(); // Or a more user-friendly message + System.exit(1)
}
}
} catch (InvalidScrambleException e1) {
e1.printStackTrace();
System.err.println("Error: Invalid scramble string: " + e1.getMessage());
// e1.printStackTrace(); // Optionally print stack trace for more detail
System.exit(1);
}
}
}
Expand Down
125 changes: 125 additions & 0 deletions src/test/java/tnoodlecli/commands/DrawCommandTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package tnoodlecli.commands;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;

import com.beust.jcommander.JCommander;
import tnoodlecli.commands.ScrambleCommand; // Added import

/**
* Unit tests for the DrawCommand class.
*/
public class DrawCommandTest {

@Test
void testDrawCommand_readsFromStdin() {
String scrambleString = "R U R' U'";
String puzzleName = "THREE"; // Corresponds to 3x3x3 cube

InputStream originalIn = System.in;
PrintStream originalOut = System.out;

try {
// Simulate stdin
System.setIn(new ByteArrayInputStream(scrambleString.getBytes()));

// Capture stdout
ByteArrayOutputStream baos = new ByteArrayOutputStream();
System.setOut(new PrintStream(baos));

// Execute the command
DrawCommand drawCommand = new DrawCommand();

// Use JCommander to parse arguments and set the 'puzzle' field.
// 'scramble' field will remain null as it's not provided.
JCommander.newBuilder()
.addObject(drawCommand)
.build()
.parse("-p", puzzleName);

drawCommand.run();

// Assert the output
String output = baos.toString();
assertNotNull(output, "Output should not be null.");
assertFalse(output.isEmpty(), "Output should not be empty.");
assertTrue(output.contains("<svg"), "Output should contain '<svg'.");
assertTrue(output.contains("</svg>"), "Output should contain '</svg>'.");
assertTrue(output.contains("width="), "Output should contain 'width='.");
assertTrue(output.contains("height="), "Output should contain 'height='.");
// Check for potential error messages instead of SVG content
assertFalse(output.contains("Error:"), "Output should not contain 'Error:'.");

} finally {
// Restore System.in and System.out
System.setIn(originalIn);
System.setOut(originalOut);
}
}
}

@Test
void testFullPipeWorkflow_scrambleToDraw() {
String puzzleName = "THREE"; // Corresponds to 3x3x3 cube

InputStream originalIn = System.in;
PrintStream originalOut = System.out;
ByteArrayOutputStream scrambleBaos = new ByteArrayOutputStream();
ByteArrayOutputStream drawBaos = new ByteArrayOutputStream();

try {
// === ScrambleCommand Phase ===
System.setOut(new PrintStream(scrambleBaos));

ScrambleCommand scrambleCommand = new ScrambleCommand();
JCommander.newBuilder()
.addObject(scrambleCommand)
.build()
.parse("-p", puzzleName, "-c", "1"); // Set puzzle and count to 1

scrambleCommand.run();
String generatedScramble = scrambleBaos.toString().trim(); // Trim to remove trailing newline

// Restore System.out before DrawCommand potentially prints errors to console during setup
System.setOut(originalOut);

// Ensure scramble is not empty
assertNotNull(generatedScramble, "Generated scramble should not be null.");
assertFalse(generatedScramble.isEmpty(), "Generated scramble should not be empty.");

// === DrawCommand Phase ===
System.setIn(new ByteArrayInputStream(generatedScramble.getBytes()));
System.setOut(new PrintStream(drawBaos)); // Capture DrawCommand output

DrawCommand drawCommand = new DrawCommand();
JCommander.newBuilder()
.addObject(drawCommand)
.build()
.parse("-p", puzzleName); // Set puzzle, scramble comes from stdin

drawCommand.run();
String svgOutput = drawBaos.toString();

// Assert the SVG output
assertNotNull(svgOutput, "SVG output should not be null.");
assertFalse(svgOutput.isEmpty(), "SVG output should not be empty.");
assertTrue(svgOutput.contains("<svg"), "SVG output should contain '<svg'.");
assertTrue(svgOutput.contains("</svg>"), "SVG output should contain '</svg>'.");
assertTrue(svgOutput.contains("width="), "SVG output should contain 'width='.");
assertTrue(svgOutput.contains("height="), "SVG output should contain 'height='.");
assertFalse(svgOutput.contains("Error:"), "SVG output should not contain 'Error:'.");

} finally {
// Restore System.in and System.out
System.setIn(originalIn);
System.setOut(originalOut);
}
}
}