mod helpers;
mod reader;
mod writer;
pub use reader::SokReader;
pub use writer::SokWriter;
#[cfg(test)]
mod tests {
use std::fs::File;
use std::io::{BufReader, Cursor};
use std::path::PathBuf;
use indoc::indoc;
use tempfile::NamedTempFile;
use super::super::file_format::{FileFormatReader, FileFormatWriter};
use super::super::{IoReadError, WriterSettings};
use super::*;
use crate::utilities::is_blank;
use crate::{SokobanPlus, Tessellation};
macro_rules! tests_data_dir {
() => {
concat!(env!("CARGO_MANIFEST_DIR"), "/src/io/tests_data/")
};
}
#[test]
fn it_reads_collection_from_file() -> Result<(), IoReadError> {
let collection = {
let data_dir = PathBuf::from(tests_data_dir!());
let src_file_path = data_dir.join("Original_and_Extra.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(None, true).read(&mut file)?
};
assert_eq!(collection.title, "Original & Extra");
assert_eq!(collection.author, "Thinking Rabbit");
assert_eq!(collection.created_at, "2022-07-01 01:05:06");
assert_eq!(collection.updated_at, "2022-07-02 09:48:22");
assert_eq!(
collection.notes,
["Originate from: original.slc", "Some collection notes",].join("\n")
);
assert_eq!(collection.puzzles.len(), 91);
let puzzle = &collection.puzzles[0];
assert_eq!(puzzle.title, "Level 1");
assert_eq!(puzzle.author, "author name");
assert_eq!(puzzle.notes, ["Time: 00:01:00", "", "Some puzzle notes"].join("\n"));
assert!(puzzle.sokoban_plus.is_some());
if let Some(sp) = &puzzle.sokoban_plus {
assert_eq!(sp.boxorder(), "1 2 3 4 5 6");
assert_eq!(sp.goalorder(), "6 5 4 3 2 1");
}
let board = indoc! {" #####
# #
#$ #
### $##
# $ $ #
### # ## # ######
# # ## ##### ..#
# $ $ ..#
##### ### #@## ..#
# #########
#######"};
assert_eq!(puzzle.board.printed(false), board);
if puzzle.board.tessellation() == Tessellation::Sokoban {
assert!(!puzzle.saved_games.is_empty());
let saved_game = &puzzle.saved_games[0];
assert_eq!(
saved_game.notes,
[
"Some save game notes",
"Optimizer: YASS 2.86",
"Time: 00:00:47",
]
.join("\n")
);
assert_eq!(
saved_game.printed_steps(),
[
"ullluuuLUllDlldddrRRRRRRRRRRRRlllllllllllllulldRRRRRRRRRRRRRdrUlllllll",
"uuululldDDuulldddrRRRRRRRRRRuRRlDllllllluuulLulDDDuulldddrRRRRRRRRRRdR",
"RlUllllllluuulluuurDDluulDDDDDuulldddrRRRRRRRRRRRllllllluuuLLulDDDuull",
"dddrRRRRRRRRRRuRDldR"
]
.join("")
);
assert_eq!(saved_game.solved_by, "only human");
} else {
return Err(IoReadError::from(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"PuzzleStorage should have at least one Puzzle after parsing",
)));
}
Ok(())
}
#[test]
fn it_loads_puzzle_without_title() -> Result<(), IoReadError> {
let collection = {
let src_file_path =
PathBuf::from(tests_data_dir!()).join("parser_test_puzzle_no_title.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(None, true).read(&mut file)?
};
assert!(is_blank(&collection.puzzles[0].title));
assert!(!is_blank(&collection.puzzles[0].board.printed(false)));
Ok(())
}
#[test]
fn it_loads_puzzle_with_multiple_saved_games() -> Result<(), IoReadError> {
let collection = {
let src_file_path =
PathBuf::from(tests_data_dir!()).join("parser_test_multiple_saved_games.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(None, true).read(&mut file)?
};
if collection.puzzles[0].board.tessellation() == Tessellation::Sokoban {
assert_eq!(collection.puzzles[0].saved_games.len(), 3);
assert_eq!(
collection.puzzles[0].saved_games[1].title,
[
"This is note for first save game, but parser can't know that and should ",
"recognize it as heading of next save game."
]
.join("")
);
}
Ok(())
}
#[test]
fn it_loads_last_saved_game_notes() -> Result<(), IoReadError> {
let collection = {
let src_file_path =
PathBuf::from(tests_data_dir!()).join("parser_test_last_saved_game_notes.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(None, true).read(&mut file)?
};
assert_eq!(
collection.puzzles[1].title,
"This is note for last save game, not heading line"
);
assert_eq!(
collection.puzzles[1].notes,
"but parser can't know that and should recognize it as heading line"
);
Ok(())
}
#[test]
fn it_correctly_loads_mixed_tessellations_collection() -> Result<(), IoReadError> {
let collection = {
let src_file_path = PathBuf::from(tests_data_dir!()).join("mixed_collection.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(None, true).read(&mut file)?
};
assert_eq!(
collection
.puzzles
.iter()
.filter(|p| { p.board.tessellation() == Tessellation::Sokoban })
.count(),
4
);
assert_eq!(
collection
.puzzles
.iter()
.filter(|p| { p.board.tessellation() == Tessellation::Hexoban })
.count(),
5
);
Ok(())
}
#[test]
fn it_defaults_to_sokoban_if_not_specified() -> Result<(), IoReadError> {
let collection = {
let src_file_path =
PathBuf::from(tests_data_dir!()).join("parser_test_variant_type_not_specified.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(None, true).read(&mut file)?
};
for puzzle in &collection.puzzles {
assert!(puzzle.board.tessellation() == Tessellation::Sokoban);
}
Ok(())
}
#[test]
fn it_uses_tessellation_value_from_collection_notes_even_if_hint_given()
-> Result<(), IoReadError> {
let collection = {
let src_file_path = PathBuf::from(tests_data_dir!())
.join("parser_test_variant_type_specified_global.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(Some(Tessellation::Octoban), true).read(&mut file)?
};
for puzzle in &collection.puzzles {
assert!(puzzle.board.tessellation() == Tessellation::Trioban);
}
Ok(())
}
#[test]
fn it_uses_tessellation_value_from_puzzle_notes_and_ignores_hint_and_collection_notes()
-> Result<(), IoReadError> {
let collection = {
let src_file_path = PathBuf::from(tests_data_dir!())
.join("parser_test_variant_type_specified_puzzle1.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(Some(Tessellation::Octoban), true).read(&mut file)?
};
assert!(collection.puzzles[0].board.tessellation() == Tessellation::Sokoban);
assert!(collection.puzzles[1].board.tessellation() == Tessellation::Hexoban);
Ok(())
}
#[test]
fn it_uses_value_from_hint_when_nothing_is_specified_in_file() -> Result<(), IoReadError> {
let collection = {
let src_file_path = PathBuf::from(tests_data_dir!())
.join("parser_test_variant_type_specified_puzzle2.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(Some(Tessellation::Trioban), true).read(&mut file)?
};
assert!(collection.puzzles[0].board.tessellation() == Tessellation::Trioban);
assert!(collection.puzzles[1].board.tessellation() == Tessellation::Hexoban);
Ok(())
}
#[test]
fn it_writes_collection_to_file() -> Result<(), Box<dyn std::error::Error>> {
let original = {
let src_file_path = PathBuf::from(tests_data_dir!()).join("Original_and_Extra.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(None, true).read(&mut file)?
};
let saved = {
let mut file1 = NamedTempFile::new()?;
let mut file2 = BufReader::new(file1.reopen()?);
SokWriter::new(None, false, WriterSettings::default())
.write_collection(&original, &mut file1)?;
SokReader::new(None, true).read(&mut file2)?
};
assert_eq!(original.puzzles.len(), saved.puzzles.len());
assert_eq!(original.puzzles, saved.puzzles);
Ok(())
}
#[test]
fn it_reads_99_as_a_plus_id_unless_legacy_sokoban_plus_is_set()
-> Result<(), Box<dyn std::error::Error>> {
let data = indoc! {"
#####
#@$.#
#####
boxorder: 99 3
goalorder: 3 99
"};
let collection = SokReader::new(None, false).read(&mut Cursor::new(data.as_bytes()))?;
let sokoban_plus = collection.puzzles[0]
.sokoban_plus
.as_ref()
.expect("Sokoban+");
assert_eq!(sokoban_plus.boxorder(), "99 3");
let collection = SokReader::new(None, true).read(&mut Cursor::new(data.as_bytes()))?;
let sokoban_plus = collection.puzzles[0]
.sokoban_plus
.as_ref()
.expect("Sokoban+");
assert_eq!(sokoban_plus.boxorder(), "0 3");
Ok(())
}
#[test]
fn it_writes_sokoban_plus_interior_gaps_to_file() -> Result<(), Box<dyn std::error::Error>> {
let mut original = {
let src_file_path = PathBuf::from(tests_data_dir!()).join("Original_and_Extra.sok");
let mut file = BufReader::new(File::open(src_file_path)?);
SokReader::new(None, true).read(&mut file)?
};
original.puzzles[0].sokoban_plus = Some(SokobanPlus::parsed("1 0 0 2", "2 0 0 1")?);
let mut file1 = NamedTempFile::new()?;
let mut file2 = BufReader::new(file1.reopen()?);
SokWriter::new(None, false, WriterSettings::default())
.write_collection(&original, &mut file1)?;
let written = std::fs::read_to_string(file1.path())?;
assert!(written.contains("boxorder: 1 0 0 2"), "{written}");
assert!(written.contains("goalorder: 2 0 0 1"), "{written}");
let saved = SokReader::new(None, true).read(&mut file2)?;
assert_eq!(original.puzzles, saved.puzzles);
Ok(())
}
}