From 588099d1f893ebd31267f2f8c38e6836a66bb6c6 Mon Sep 17 00:00:00 2001 From: zvon Date: Fri, 8 May 2020 21:11:51 +0000 Subject: [PATCH] Add tests and fix errors tests have discovered --- .gitlab-ci.yml | 12 - Makefile | 2 +- filesystem.hpp | 1 + functions.cpp | 45 ++- progress.cpp | 8 +- tests/test.cpp | 712 ++++++++++++++++++++++++++++++++++++++++- tv_rename.cpp | 22 +- tv_rename.hpp | 4 +- unix/filesystem.cpp | 28 ++ windows/filesystem.cpp | 25 ++ 10 files changed, 806 insertions(+), 53 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d5fb1a2..3c13504 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,17 +20,6 @@ linux:build: - tv_rename_gui expire_in: 30 min -linux:test: - image: debian - stage: test - needs: ["linux:build"] - - before_script: - - apt update - - apt install -y libcurl4 sqlite3 - script: - - ./tv_rename --help - linux:codacy: image: debian stage: test @@ -43,7 +32,6 @@ linux:codacy: script: - make test.out - make check - after_script: - find . -type f -name "*.gcno" -execdir gcov -pb -r {} + - gcovr --root . -k -j 2 --xml -o gcovr_report.xml --exclude-directories "tests" --exclude-directories "gtk" --exclude-directories "win*" --exclude-directories "sqlite-am*" --exclude-directories "rapidjson" - bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r "gcovr_report.xml" --language CPP --force-language diff --git a/Makefile b/Makefile index 58b39b6..a1f68c4 100644 --- a/Makefile +++ b/Makefile @@ -188,7 +188,7 @@ locale/%/LC_MESSAGES/tv_rename.mo: translations/%.po locale/%/LC_MESSAGES test.out: tests/test.cpp functions.cpp unix/network.cpp progress.cpp\ unix/filesystem.cpp tv_rename.cpp - $(CXX) $^ -lcurl -lsqlite3 --coverage -o test.out + $(CXX) $^ -lcurl -lsqlite3 --coverage -g -fsanitize=address,undefined -o test.out .PHONY: check check: test.out diff --git a/filesystem.hpp b/filesystem.hpp index 2a64341..5477673 100644 --- a/filesystem.hpp +++ b/filesystem.hpp @@ -28,6 +28,7 @@ bool exists( const string &path ); bool isDirectory( const string &path ); bool rename( const string &file_a, const string &file_b ); string canonical( const string &path ); +bool createDirectoryFull( const string &path ); class Directory { public: diff --git a/functions.cpp b/functions.cpp index d8d202b..6e077e9 100644 --- a/functions.cpp +++ b/functions.cpp @@ -56,27 +56,20 @@ std::wstring LMsg( int id, ... ) { static wchar_t local[MAX_PATH]; auto hInstance = GetModuleHandle( NULL ); LoadString( hInstance, id, local, MAX_PATH ); - va_list args; - va_start( args, id ); - int count = 0; - const wchar_t *p; - while ( ( p = va_arg( args, const wchar_t * ) ) != NULL ) - count++; - va_end( args ); - - if ( count == 0 ) - return local; va_start( args, id ); // _vscprintf doesn't count ending '\0' int len = _vscwprintf( local, args ) + 1; va_end( args ); + wchar_t *text = new wchar_t[len]; va_start( args, id ); vswprintf( text, len + 1, local, args ); + va_end( args ); std::wstring ret = text; delete[] text; + return ret; } @@ -85,24 +78,18 @@ std::wstring LMsg( int id, ... ) { std::string getlocalized( const char *id, ... ) { const char *local = gettext( id ); va_list args; - va_start( args, id ); - int count = 0; - const char *p; - while ( ( p = va_arg( args, const char * ) ) != NULL ) - count++; - va_end( args ); - - if ( count == 0 ) - return local; va_start( args, id ); - int len = vsnprintf( nullptr, 0, local, args ); + int len = vsnprintf( nullptr, 0, local, args ) + 1; va_end( args ); - char *text = new char[len + 1]; + + char *text = new char[len]; va_start( args, id ); vsnprintf( text, len + 1, local, args ); + va_end( args ); std::string ret = text; delete[] text; + return ret; } @@ -142,10 +129,14 @@ bool searchSeason( const char_t *const path, size_t &season_pos, if ( ( path[cur_pos] == 's' || path[cur_pos] == 'S' ) && iswdigit( path[cur_pos + 1] ) ) { cur_pos++; + if ( path[cur_pos] == '\0' ) + return false; season_pos = cur_pos; // after ++ because we want the first pos to // point to season's number while ( iswdigit( path[cur_pos] ) ) cur_pos++; + if ( path[cur_pos] == '\0' ) + return false; if ( ( path[cur_pos] == 'e' || path[cur_pos] == 'E' ) && iswdigit( path[cur_pos + 1] ) ) { ep_pos = cur_pos + 1; @@ -228,7 +219,7 @@ void printPatternHelp() { cout << " %season - " << _( PATTERN_SEASON ) << std::endl; cout << " " << _( PATTERN_LEADING_ZERO ) << std::endl; cout << " %2season " << _( PATTERN_LEADING_NUM ) << std::endl; - cout << " %episode - " << _( PATTERN_EPISODE ) << std::endl; + cout << " %episode - " << _( PATTERN_EPISODE ) << std::endl; cout << " " << _( PATTERN_LEADING_ZERO ) << std::endl; cout << " %2episode " << _( PATTERN_LEADING_NUM ) << std::endl; cout << _( PATTERN_DEFAULT ) << " \"%filename - %epname\"" << std::endl; @@ -426,11 +417,17 @@ string sanitize( const string &str ) { } void prepareDB( const string &_pattern ) { + auto dbPath = getDBName(); + auto dbDir = + dbPath.substr( 0, dbPath.find_last_of( TEXT( "/" ), dbPath.length() ) ); + if ( !FSLib::exists( dbDir ) ) + FSLib::createDirectoryFull( dbDir ); + SQLite::Database db{}; try { - db.open( getDBName(), SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE ); + db.open( dbPath, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE ); } catch ( std::exception &e ) { - cerr << _( DB_NOT_CREATE ) << " " << getDBName() << std::endl; + cerr << _( DB_NOT_CREATE ) << " " << dbPath << std::endl; throw; } db.exec( diff --git a/progress.cpp b/progress.cpp index 7ba6992..fd5fed5 100644 --- a/progress.cpp +++ b/progress.cpp @@ -30,14 +30,14 @@ #ifndef GUI // Don't need terminal specific functions in GUI #ifdef _WIN32 -size_t getTermWidth() { +int getTermWidth() { CONSOLE_SCREEN_BUFFER_INFO csbi; GetConsoleScreenBufferInfo( GetStdHandle( STD_OUTPUT_HANDLE ), &csbi ); return csbi.srWindow.Right - csbi.srWindow.Left + 1; } #else // UNIX -size_t getTermWidth() { +int getTermWidth() { struct winsize w; ioctl( 0, TIOCGWINSZ, &w ); @@ -63,7 +63,7 @@ void home( size_t width ) { #endif // not GUI -size_t getNum( size_t width, int perc ) { +int getNum( int width, int perc ) { return ( width * perc ) / 100; } @@ -72,6 +72,8 @@ void ProgressBar::print( int perc ) { auto width = getTermWidth(); home( width ); auto count = getNum( width - 7, perc ); + if ( count < 0 ) + count = 0; cout << "[" << string( count, TEXT( '=' ) ).c_str(); if ( perc != 100 ) { int wc = width - 8 - count; diff --git a/tests/test.cpp b/tests/test.cpp index bf6a0ca..89ea47b 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -1,8 +1,51 @@ #define CATCH_CONFIG_MAIN #include "catch.hpp" + +#include "../filesystem.hpp" #include "../functions.hpp" +#include "../resources_linux.h" +#include "../tv_rename.hpp" +#include "../sqlitepp.hpp" +#include #include +// declaring functions that aren't in functions.hpp + +std::string sanitize( const std::string &str ); +std::string getDBName(); +void cleanUpLine(); +std::vector< std::tuple< int, string, string, string > > + +getRenamedFiles( const string &show, int season, const string &id, + const string &language, const string &pattern, + const bool &unix_names, const std::map< int, string > &files, + bool dvd ); +std::map< string, string > getLangs(); + +// functions required for testing + +bool authenticated = false; + +void testDB() { + if ( !FSLib::exists( getDBName() ) ) { + prepareDB( "S%2seasonE%2episode - %epname" ); + } +} + +bool runBash( const std::string &command ) { + std::string cmd = "/bin/bash -c \"" + command + "\""; + return system( cmd.c_str() ) == 0; +} + +void authenticateTest() { + if ( !authenticated ) { + authenticate( "42B66F5E-C6BF-423F-ADF9-CC97163472F6" ); + authenticated = true; + } +} + +// functions.hpp + TEST_CASE( "encodeUrl" ) { std::string url1 = "http://google.com"; std::string url1_expected = "http%3A%2F%2Fgoogle.com"; @@ -16,17 +59,676 @@ TEST_CASE( "encodeUrl" ) { TEST_CASE( "userHome" ) { SECTION( "correct usage" ) { auto home = userHome(); - std::string command = "/bin/bash -c \"if [ \"${HOME}\" == \""; + std::string command = "if [ \"\\${HOME}\" == \""; command += home; - command += "\" ] ; then exit 0 ; else exit 1 ; fi\""; - REQUIRE( system( command.c_str() ) == 0 ); + command += "\" ] ; then exit 0 ; else exit 1 ; fi"; + REQUIRE( runBash( command ) ); } + uid_t original_u{ 0 }, original_e{ 0 }, original_s{ 0 }; + gid_t original_g{ 0 }, original_ge{ 0 }, original_gs{ 0 }; + + getresuid( &original_u, &original_e, &original_s ); + getresgid( &original_g, &original_ge, &original_gs ); + SECTION( "exception with non existing user" ) { - setresgid( 1025, 1025, 1025 ); - setresuid( 1025, 1025, 1025 ); + setresuid( 1025, 1025, -1 ); REQUIRE_THROWS_WITH( userHome(), Catch::Matchers::Contains( "User with uid" ) && Catch::Matchers::Contains( "doesn't exist!" ) ); } + + setresuid( original_u, original_e, original_s ); +} + +TEST_CASE( "getlocalized" ) { + auto ret = getlocalized( ADDING_TO_DB ); + REQUIRE( ret == "Adding to database" ); + ret = getlocalized( UPDATING_IN_DB, "test" ); + REQUIRE( ret == "Updating test in database" ); +} + +TEST_CASE( "getDBPattern" ) { + testDB(); + REQUIRE( "S%2seasonE%2episode - %epname" == getDBPattern() ); +} + +TEST_CASE( "searchSeason" ) { + size_t seasonpos{ 0 }, eppos{ 0 }; + REQUIRE( searchSeason( "s001e003", seasonpos, eppos ) ); + REQUIRE( ( seasonpos == 1 && eppos == 5 ) ); + REQUIRE( searchSeason( "s001e003", seasonpos ) ); + REQUIRE( seasonpos == 1 ); + REQUIRE_FALSE( searchSeason( "e01S013", seasonpos, eppos ) ); + REQUIRE( + searchSeason( "TesTSOIHSADF0239s023S23iewahoiehwf001e22E33sS1E1.mkv", + seasonpos, eppos ) ); + REQUIRE( ( seasonpos == 45 && eppos == 47 ) ); + REQUIRE_FALSE( searchSeason( "s01sE01.mkv", seasonpos ) ); +} + +TEST_CASE( "iterateFS" ) { + runBash( + "mkdir test_iteratefs ; cd test_iteratefs ; for f in {01..10} ; do " + "mkdir \\\"Season \\${f}\\\" ; cd \\\"Season \\${f}\\\" ; for g in " + "{01..30} ; do touch \\\"S\\${f}E\\${g}.mkv\\\" ; done ; cd .. ; " + "done" ); + std::map< int, std::map< int, string > > seasons; + iterateFS( seasons, "test_iteratefs" ); + REQUIRE( seasons.size() == 10 ); + for ( auto &x : seasons ) { + REQUIRE( x.second.size() == 30 ); + for ( auto &ep : x.second ) { + std::string epfile = "S"; + if ( x.first <= 9 ) + epfile += "0"; + epfile += std::to_string( x.first ); + epfile += "E"; + if ( ep.first <= 9 ) + epfile += "0"; + epfile += std::to_string( ep.first ); + epfile += ".mkv"; + REQUIRE_THAT( ep.second, Catch::Contains( epfile ) ); + } + } + + REQUIRE_THROWS_WITH( iterateFS( seasons, "nonexistendir" ), + Catch::Contains( "Directory" ) && + Catch::Contains( "doesn't exist" ) ); + REQUIRE_THROWS_WITH( + iterateFS( seasons, "test_iteratefs/Season 01/S01E01.mkv" ), + Catch::Contains( "Directory" ) && + Catch::Contains( "isn't a directory" ) ); +} + +TEST_CASE( "printing functions" ) { + std::string expectedHelp = + "Usage:\n tv_rename [options] [path]\n\n -h, --help " + "show this help message and exit\n\n path can be either a file or a " + "directory, if it's a directory\n all files in it and its " + "subdirectories will be renamed\n\nOPTIONS\n -s, --show " + "TV show from which you want the episode names\n -n, --season " + " Season number/s (if multiple seasons,\n " + " must be seperated by one space) or 'all'\n " + " for all seasons in selected directory\n\n -d, --dvd " + " use dvd ordering instead of aired ordering\n --name-pattern " + " Pattern to which change the file name.\n --pattern-help " + " Print pattern help\n -c, --correct-path This is the " + "correct path, stop asking me!\n -t, --trust Don't ask " + "whether the names are correct\n -x, --linux Don't " + "replace characters characters that are\n " + "illegal in Windows\n -l, --lang Select which language " + "the episode names shoud be in\n --print-langs Print " + "available languages\n\nDATABASE OPTIONS\n --db-add " + "Add path to the database\n --db-refresh Refresh " + "episode names for all paths in the database\n --db-update " + " Check all paths in the database,\n if " + "they contain new files, rename them\n --db-name-pattern " + "Change name pattern used for files\n " + "managed by database\n --db-clean Remove deleted " + "files from the database\n --db-remove Remove path " + "from the database\n"; + + std::string expectedPattern = + "Possible pattern sequences are:\n %filename - original filename " + "(without filetype extension)\n %show - show name from thetvdb\n " + "%epname - episode name from thetvdb\n %season - season number\n " + "it's possible to specify leading 0 like this:\n %2season (number " + "means how many zeros)\n %episode - episode number\n it's possible " + "to specify leading 0 like this:\n %2episode (number means how " + "many zeros)\nDefault pattern is \"%filename - %epname\"\n"; + + std::ostringstream oss; + auto original_cout = std::cout.rdbuf(); + std::cout.rdbuf( oss.rdbuf() ); + printHelp(); + REQUIRE( oss.str() == expectedHelp ); + + oss.str( "" ); + oss.clear(); + printPatternHelp(); + REQUIRE( oss.str() == expectedPattern ); + + std::cout.rdbuf( original_cout ); +} + +TEST_CASE( "parseSeasonNumber" ) { + std::set< int > season_nums{}; + parseSeasonNumbers( season_nums, "1 2 3 4 5 6" ); + REQUIRE( season_nums.size() == 6 ); + for ( int i = 1; i < 7; i++ ) { + REQUIRE( season_nums.find( i ) != season_nums.end() ); + } + season_nums.clear(); + + parseSeasonNumbers( season_nums, "this is not a number" ); + REQUIRE( season_nums.size() == 0 ); + + parseSeasonNumbers( season_nums, + "this is not a number 1 2 3 this is the end 4 5 6" ); + REQUIRE( season_nums.size() == 3 ); +} + +std::string testCompilePattern( const std::string &pattern ) { + return compilePattern( pattern, 45, 914, "episode_of_show_s45e914", + "The one where everyone dies", "Enemies" ); +} + +TEST_CASE( "compilePattern" ) { + REQUIRE( testCompilePattern( "%filename" ) == "episode_of_show_s45e914" ); + REQUIRE( testCompilePattern( "%show" ) == "Enemies" ); + REQUIRE( testCompilePattern( "%epname" ) == "The one where everyone dies" ); + REQUIRE( testCompilePattern( "%season" ) == "45" ); + REQUIRE( testCompilePattern( "%episode" ) == "914" ); + REQUIRE( testCompilePattern( "%1season" ) == + testCompilePattern( "%2season" ) ); + REQUIRE( testCompilePattern( "%20season" ) == "00000000000000000045" ); + REQUIRE( testCompilePattern( "%1episode" ) == + testCompilePattern( "%2episode" ) ); + REQUIRE( testCompilePattern( "%20episode" ) == "00000000000000000914" ); + REQUIRE( testCompilePattern( + "%filename - %show - S%3seasonE%3episode - %epname" ) == + "episode_of_show_s45e914 - Enemies - S045E914 - The one where " + "everyone dies" ); + REQUIRE( testCompilePattern( "\\%filename" ) == "%filename" ); + REQUIRE( testCompilePattern( "\\\\%filename" ) == + "\\episode_of_show_s45e914" ); + REQUIRE( testCompilePattern( "\\filename" ) == "\\filename" ); + REQUIRE( testCompilePattern( "%notakeyword" ) == "%notakeyword" ); +} + +TEST_CASE( "sanitize" ) { + REQUIRE( sanitize( "I am a nice input" ) == "I am a nice input" ); + REQUIRE( + sanitize( "I am an evil input!'; DROP TABLE USER_ACCOUNT_BALANCE;" ) == + "I am an evil input!''; DROP TABLE USER_ACCOUNT_BALANCE;" ); +} + +TEST_CASE( "cleanUpLine" ) { + std::ostringstream oss; + auto original_cout = std::cout.rdbuf(); + std::cout.rdbuf( oss.rdbuf() ); + + cleanUpLine(); + std::cout << std::flush; + + REQUIRE_THAT( oss.str().substr( 3 ), Catch::Contains( "\x1b[2A" ) ); + + std::cout.rdbuf( original_cout ); +} + +TEST_CASE( "addToDB" ) { + testDB(); + authenticateTest(); + std::istringstream iss; + auto original_cin = std::cin.rdbuf(); + std::cin.rdbuf( iss.rdbuf() ); + std::cout.setstate( std::ios_base::failbit ); + iss.str( "1\n" ); + + runBash( "mkdir add_dir ; touch add_dir/s01e01.mkv" ); + + std::string show = "simpsons"; + addToDB( show, "add_dir", "en", true, false ); + + SQLite::Database db{}; + db.open( getDBName(), SQLite::OPEN_READONLY ); + + std::vector< std::unordered_map< std::string, std::string > > result_eps{}; + std::vector< std::unordered_map< std::string, std::string > > + result_shows{}; + db.exec( "SELECT * FROM EPISODES;", result_eps ); + db.exec( "SELECT * FROM SHOWS;", result_shows ); + + std::string wanted_id{}; + + for ( auto &x : result_shows ) { + if ( x["SHOW"] == "The Simpsons" ) { + wanted_id = x["ID"]; + break; + } + } + + bool foundCorrect = false; + for ( auto &x : result_eps ) { + if ( x["PATH"].find( + "S01E01 - Simpsons Roasting on an Open Fire.mkv" ) != + std::string::npos && + x["SHOWID"] == wanted_id ) { + foundCorrect = true; + break; + } + } + + REQUIRE( foundCorrect ); + + std::cin.rdbuf( original_cin ); + std::cout.clear(); +} + +TEST_CASE( "refreshDB" ) { + testDB(); + authenticateTest(); + std::istringstream iss; + auto original_cin = std::cin.rdbuf(); + std::cin.rdbuf( iss.rdbuf() ); + std::cout.setstate( std::ios_base::failbit ); + iss.str( "1\n" ); + + runBash( "mkdir refresh_dir ; touch refresh_dir/s01e01.mkv" ); + + std::string show = "friends"; + addToDB( show, "refresh_dir", "en", true, false ); + runBash( "touch refresh_dir/s01e02.mkv" ); + refreshDB( true ); + + SQLite::Database db{}; + db.open( getDBName(), SQLite::OPEN_READONLY ); + + std::vector< std::unordered_map< std::string, std::string > > result_eps{}; + std::vector< std::unordered_map< std::string, std::string > > + result_shows{}; + db.exec( "SELECT * FROM EPISODES;", result_eps ); + db.exec( "SELECT * FROM SHOWS;", result_shows ); + + std::string wanted_id{}; + + for ( auto &x : result_shows ) { + if ( x["SHOW"] == "Friends" ) { + wanted_id = x["ID"]; + break; + } + } + + bool foundCorrect = false; + for ( auto &x : result_eps ) { + if ( x["PATH"].find( + "S01E02 - The One With The Sonogram At The End.mkv" ) != + std::string::npos && + x["SHOWID"] == wanted_id ) { + foundCorrect = true; + break; + } + } + + REQUIRE( foundCorrect ); + + std::cin.rdbuf( original_cin ); + std::cout.clear(); +} + +TEST_CASE( "updateDB" ) { + testDB(); + authenticateTest(); + std::istringstream iss; + auto original_cin = std::cin.rdbuf(); + std::cin.rdbuf( iss.rdbuf() ); + std::cout.setstate( std::ios_base::failbit ); + iss.str( "1\n" ); + + runBash( "mkdir update_dir ; touch update_dir/s01e01.mkv" ); + + std::string show = "seinfeld"; + addToDB( show, "update_dir", "en", true, false ); + runBash( "touch update_dir/s01e02.mkv" ); + updateDB( true ); + + SQLite::Database db{}; + db.open( getDBName(), SQLite::OPEN_READONLY ); + + std::vector< std::unordered_map< std::string, std::string > > result_eps{}; + std::vector< std::unordered_map< std::string, std::string > > + result_shows{}; + db.exec( "SELECT * FROM EPISODES;", result_eps ); + db.exec( "SELECT * FROM SHOWS;", result_shows ); + + std::string wanted_id{}; + + for ( auto &x : result_shows ) { + if ( x["SHOW"] == "Seinfeld" ) { + wanted_id = x["ID"]; + break; + } + } + + bool foundCorrect = false; + for ( auto &x : result_eps ) { + if ( x["PATH"].find( "S01E02 - The Stake Out.mkv" ) != + std::string::npos && + x["SHOWID"] == wanted_id ) { + foundCorrect = true; + break; + } + } + + REQUIRE( foundCorrect ); + + std::cin.rdbuf( original_cin ); + std::cout.clear(); +} + +TEST_CASE( "cleanDB" ) { + testDB(); + authenticateTest(); + std::istringstream iss; + auto original_cin = std::cin.rdbuf(); + std::cin.rdbuf( iss.rdbuf() ); + std::cout.setstate( std::ios_base::failbit ); + iss.str( "1\n" ); + + runBash( "mkdir clean_dir ; touch clean_dir/s01e01.mkv" ); + + std::string show = "scrubs"; + addToDB( show, "clean_dir", "en", true, false ); + runBash( "rm clean_dir/S01E01*" ); + cleanDB(); + + SQLite::Database db{}; + db.open( getDBName(), SQLite::OPEN_READONLY ); + + std::vector< std::unordered_map< std::string, std::string > > result_eps{}; + std::vector< std::unordered_map< std::string, std::string > > + result_shows{}; + db.exec( "SELECT * FROM EPISODES;", result_eps ); + db.exec( "SELECT * FROM SHOWS;", result_shows ); + + std::string wanted_id{}; + + for ( auto &x : result_shows ) { + if ( x["SHOW"] == "Scrubs" ) { + wanted_id = x["ID"]; + break; + } + } + + bool found = false; + for ( auto &x : result_eps ) { + if ( x["PATH"].find( "S01E01 - My First Day.mkv" ) != + std::string::npos && + x["SHOWID"] == wanted_id ) { + found = true; + break; + } + } + + REQUIRE_FALSE( found ); + + std::cin.rdbuf( original_cin ); + std::cout.clear(); +} + +TEST_CASE( "removeFromDB" ) { + testDB(); + authenticateTest(); + std::istringstream iss; + auto original_cin = std::cin.rdbuf(); + std::cin.rdbuf( iss.rdbuf() ); + std::cout.setstate( std::ios_base::failbit ); + iss.str( "1\n" ); + + runBash( "mkdir remove_dir ; touch remove_dir/s01e01.mkv" ); + + std::string show = "how i met"; + addToDB( show, "remove_dir", "en", true, false ); + + SQLite::Database db{}; + db.open( getDBName(), SQLite::OPEN_READONLY ); + + std::vector< std::unordered_map< std::string, std::string > > + result_shows{}; + db.exec( "SELECT * FROM SHOWS;", result_shows ); + + std::string wanted_id{ "0" }; + + for ( auto &x : result_shows ) { + if ( x["SHOW"] == "How I Met Your Mother" ) { + wanted_id = x["ID"]; + break; + } + } + + REQUIRE( wanted_id != "0" ); + removeFromDB( FSLib::canonical( "." ) + "/remove_dir" ); + wanted_id = "0"; + result_shows.clear(); + db.exec( "SELECT * FROM SHOWS;", result_shows ); + + for ( auto &x : result_shows ) { + if ( x["SHOW"] == "How I Met Your Mother" ) { + wanted_id = x["ID"]; + break; + } + } + + REQUIRE( wanted_id == "0" ); + + std::cin.rdbuf( original_cin ); + std::cout.clear(); +} + +TEST_CASE( "changeDBPattern" ) { + testDB(); + std::string old_pattern = "S%2seasonE%2episode - %epname"; + std::string new_pattern = "THIS IS NOT A GOOD PATTERN"; + REQUIRE( getDBPattern() == old_pattern ); + changeDBPattern( new_pattern ); + REQUIRE( getDBPattern() == new_pattern ); + changeDBPattern( old_pattern ); + REQUIRE( getDBPattern() == old_pattern ); +} + +// filesystem.hpp + +TEST_CASE( "Directory" ) { + std::string command = + "mkdir test_dir ; cd test_dir ; for f in {01..10} ; do " + "touch \\${f} ; done"; + runBash( command ); + FSLib::Directory d( "test_dir" ); + std::set< std::string > files = { "01", "02", "03", "04", "05", + "06", "07", "08", "09", "10" }; + int counter = 0; + for ( auto x : d ) { + counter++; + REQUIRE( files.find( x ) != files.end() ); + } + REQUIRE( counter == 10 ); +} + +TEST_CASE( "Filesystem" ) { + std::string command = + "mkdir test_dir2 ; cd test_dir2 ; for f in {01..10} ; do " + "touch \\${f} ; done"; + runBash( command ); + std::set< std::string > files = { "01", "02", "03", "04", "05", + "06", "07", "08", "09", "10" }; + + // exists + REQUIRE( FSLib::exists( "test_dir2" ) ); + for ( auto &x : files ) { + REQUIRE( FSLib::exists( "test_dir2/" + x ) ); + REQUIRE_FALSE( FSLib::exists( x ) ); + } + + // isDirectory + REQUIRE( FSLib::isDirectory( "test_dir2" ) ); + REQUIRE( FSLib::isDirectory( "." ) ); + REQUIRE_FALSE( FSLib::isDirectory( "test_dir2/01" ) ); + REQUIRE_FALSE( FSLib::isDirectory( "nonexistentfile" ) ); + + // canonical + auto pwd = FSLib::canonical( "." ); + command = "if [ \"\\${PWD}\" == \""; + command += pwd; + command += "\" ] ; then exit 0 ; else exit 1 ; fi"; + REQUIRE( runBash( command ) ); + REQUIRE( FSLib::canonical( "nonexistentfile" ) == "" ); + + // rename + for ( auto &x : files ) { + REQUIRE( FSLib::rename( "test_dir2/" + x, "test_dir2/renamed_" + x ) ); + REQUIRE_FALSE( + FSLib::rename( "test_dir2/" + x, "test_dir2/renamed_" + x ) ); + REQUIRE( FSLib::exists( "test_dir2/renamed_" + x ) ); + } +} + +// tv_rename.cpp + +TEST_CASE( "getRenamedFiles Windows chars" ) { + std::map< int, std::string > files{}; + files[1] = "I am a windows illegal file \\|<>:?\"*"; + auto res = getRenamedFiles( "simpsons", 1, "71663", "en", "%filename", + false, files, false ); + REQUIRE( std::get< 0 >( res[0] ) == 1 ); + REQUIRE( std::get< 1 >( res[0] ) == "." ); + REQUIRE( std::get< 2 >( res[0] ) == + "I am a windows illegal file \\|<>:?\"*" ); + REQUIRE( std::get< 3 >( res[0] ) == + "I am a windows illegal file -<> - " ); +} + +TEST_CASE( "getLangs" ) { + authenticateTest(); + auto langs = getLangs(); + REQUIRE_FALSE( langs.empty() ); + bool containsEn = false; + bool containsCs = false; + for ( auto &x : langs ) { + if ( x.first == "en" ) + containsEn = true; + if ( x.first == "cs" ) + containsCs = true; + } + REQUIRE( containsEn ); + REQUIRE( containsCs ); +} + +TEST_CASE( "singleSeason" ) { + runBash( + "mkdir test_singleseason ; cd test_singleseason ; for f in {01..10} ;" + "do mkdir \\\"Season \\${f}\\\" ; cd \\\"Season \\${f}\\\" ; for g in " + "{01..30} ; do touch \\\"S\\${f}E\\${g}.mkv\\\" ; done ; cd .. ; " + "done" ); + std::istringstream iss; + auto original_cin = std::cin.rdbuf(); + std::cin.rdbuf( iss.rdbuf() ); + std::cout.setstate( std::ios_base::failbit ); + iss.str( "y\n" ); + + singleSeason( "test_singleseason", "simpsons", 1, "71663", "en", + "S%2seasonE%2episode - %epname", false, false, nullptr, false, + false ); + + REQUIRE( FSLib::exists( "test_singleseason/Season 01/S01E01 - Simpsons " + "Roasting on an Open Fire.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E02 - Bart the Genius.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E03 - Homer's Odyssey.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 01/S01E04 - There's No " + "Disgrace Like Home.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E05 - Bart the General.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E06 - Moaning Lisa.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E07 - The Call of the Simpsons.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E08 - The Telltale Head.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E09 - Life on the Fast Lane.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E10 - Homer's Night Out.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E11 - The Crepes of Wrath.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E12 - Krusty Gets Busted.mkv" ) ); + REQUIRE( FSLib::exists( + "test_singleseason/Season 01/S01E13 - Some Enchanted Evening.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 02/S02E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 03/S03E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 04/S04E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 05/S05E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 06/S06E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 07/S07E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 08/S08E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 09/S09E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_singleseason/Season 10/S10E01.mkv" ) ); + + std::cin.rdbuf( original_cin ); + std::cout.clear(); +} + +TEST_CASE( "multipleSeasons" ) { + runBash( "mkdir test_multipleseasons ; cd test_multipleseasons ;" + "for f in {01..10} ; do mkdir \\\"Season \\${f}\\\" ; cd " + "\\\"Season \\${f}\\\" ; for g in " + "{01..30} ; do touch \\\"S\\${f}E\\${g}.mkv\\\" ; done ; cd .. ; " + "done" ); + std::istringstream iss; + auto original_cin = std::cin.rdbuf(); + std::cin.rdbuf( iss.rdbuf() ); + std::cout.setstate( std::ios_base::failbit ); + iss.str( "1\ny\ny\ny\n" ); + + multipleSeasons( "test_multipleseasons", "simpsons", { 1, 2, 3 }, "en", + "S%2seasonE%2episode - %epname", 0 ); + + REQUIRE( FSLib::exists( "test_multipleseasons/Season 01/S01E01 - Simpsons " + "Roasting on an Open Fire.mkv" ) ); + REQUIRE( FSLib::exists( + "test_multipleseasons/Season 02/S02E01 - Bart Gets an F.mkv" ) ); + REQUIRE( FSLib::exists( + "test_multipleseasons/Season 03/S03E01 - Stark Raving Dad.mkv" ) ); + REQUIRE( FSLib::exists( "test_multipleseasons/Season 04/S04E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_multipleseasons/Season 05/S05E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_multipleseasons/Season 06/S06E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_multipleseasons/Season 07/S07E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_multipleseasons/Season 08/S08E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_multipleseasons/Season 09/S09E01.mkv" ) ); + REQUIRE( FSLib::exists( "test_multipleseasons/Season 10/S10E01.mkv" ) ); + + std::cin.rdbuf( original_cin ); + std::cout.clear(); +} + +TEST_CASE( "allSeasons" ) { + runBash( "mkdir test_allseasons ; cd test_allseasons ;" + "for f in {01..10} ; do mkdir \\\"Season \\${f}\\\" ; cd " + "\\\"Season \\${f}\\\" ; for g in " + "{01..30} ; do touch \\\"S\\${f}E\\${g}.mkv\\\" ; done ; cd .. ; " + "done" ); + std::istringstream iss; + auto original_cin = std::cin.rdbuf(); + std::cin.rdbuf( iss.rdbuf() ); + std::cout.setstate( std::ios_base::failbit ); + iss.str( "1\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n" ); + + allSeasons( "test_allseasons", "simpsons", "en", + "S%2seasonE%2episode - %epname", 0 ); + + REQUIRE( FSLib::exists( "test_allseasons/Season 01/S01E01 - Simpsons " + "Roasting on an Open Fire.mkv" ) ); + REQUIRE( FSLib::exists( + "test_allseasons/Season 02/S02E01 - Bart Gets an F.mkv" ) ); + REQUIRE( FSLib::exists( + "test_allseasons/Season 03/S03E01 - Stark Raving Dad.mkv" ) ); + REQUIRE( + FSLib::exists( "test_allseasons/Season 04/S04E01 - Kamp Krusty.mkv" ) ); + REQUIRE( FSLib::exists( + "test_allseasons/Season 05/S05E01 - Homer's Barbershop Quartet.mkv" ) ); + REQUIRE( FSLib::exists( + "test_allseasons/Season 06/S06E01 - Bart of Darkness.mkv" ) ); + REQUIRE( FSLib::exists( + "test_allseasons/Season 07/S07E01 - Who Shot Mr. Burns (2).mkv" ) ); + REQUIRE( FSLib::exists( + "test_allseasons/Season 08/S08E01 - Treehouse of Horror VII.mkv" ) ); + REQUIRE( FSLib::exists( "test_allseasons/Season 09/S09E01 - The City of " + "New York vs. Homer Simpson.mkv" ) ); + REQUIRE( FSLib::exists( + "test_allseasons/Season 10/S10E01 - Lard of the Dance.mkv" ) ); + + std::cin.rdbuf( original_cin ); + std::cout.clear(); } diff --git a/tv_rename.cpp b/tv_rename.cpp index ae92f48..0da9bf8 100644 --- a/tv_rename.cpp +++ b/tv_rename.cpp @@ -215,11 +215,21 @@ getRenamedFiles( const string &show, int season, const string &id, if ( ep_num < episodes.size() ) { auto pos = og_name.find_last_of( TEXT( "." ) ); + string og_name_without_extension{}; + string og_name_extension{}; + + if ( pos == string::npos ) { + og_name_without_extension = og_name; + } else { + og_name_without_extension = og_name.substr( 0, pos ); + og_name_extension = og_name.substr( pos ); + } + // get desired filename auto name = compilePattern( pattern, season, x.first, - og_name.substr( 0, pos ), + og_name_without_extension, episodes[ep_num], show ) + - og_name.substr( pos ); + og_name_extension; // replace '/' with '|' for ( size_t i = 0; i < name.size(); i++ ) { if ( name[i] == '/' ) { @@ -346,7 +356,7 @@ void singleSeason( const string &path, const string &show, int season, goto end; if ( print || !trust ) { - for ( const auto renamed : renamed_files ) { + for ( const auto &renamed : renamed_files ) { cout << std::get< 2 >( renamed ) << " --> " << std::get< 3 >( renamed ) << std::endl; } @@ -363,7 +373,7 @@ void singleSeason( const string &path, const string &show, int season, } } - for ( const auto renamed : renamed_files ) { + for ( const auto &renamed : renamed_files ) { FSLib::rename( std::get< 1 >( renamed ) + _tv_rename_dir_divider + std::get< 2 >( renamed ), std::get< 1 >( renamed ) + _tv_rename_dir_divider + @@ -391,7 +401,7 @@ void singleSeason( const string &path, const string &show, int season, #ifndef GUI -void multipleSeasons( const string &path, string &show, +void multipleSeasons( const string &path, const string &show, const std::set< int > &seasons, const string &language, const string &pattern, const size_t &flags ) { std::map< int, std::map< int, string > > season_map; @@ -406,7 +416,7 @@ void multipleSeasons( const string &path, string &show, } } -void allSeasons( const string &path, string &show, const string &language, +void allSeasons( const string &path, const string &show, const string &language, const string &pattern, const size_t &flags ) { std::map< int, std::map< int, string > > seasons; // get all season number from this directory and subdirectories diff --git a/tv_rename.hpp b/tv_rename.hpp index 409f89b..30b0262 100644 --- a/tv_rename.hpp +++ b/tv_rename.hpp @@ -54,11 +54,11 @@ std::vector< std::pair< string, string > > getLangs(); #else -void multipleSeasons( const string &path, string &show, +void multipleSeasons( const string &path, const string &show, const std::set< int > &seasons, const string &language, const string &pattern, const size_t &flags ); -void allSeasons( const string &path, string &show, const string &language, +void allSeasons( const string &path, const string &show, const string &language, const string &pattern, const size_t &flags ); string getShowId( const string &show, const string &language ); diff --git a/unix/filesystem.cpp b/unix/filesystem.cpp index 706c496..cc2a0f8 100644 --- a/unix/filesystem.cpp +++ b/unix/filesystem.cpp @@ -1,11 +1,18 @@ #include "../filesystem.hpp" #include #include +#include FSLib::Directory::Directory( const string &path_ ) : dir_path( path_ ) {} FSLib::Directory::Iterator::Iterator( const Directory &d_ ) : d( opendir( d_.path() ) ) { + if ( !exists( d_.path() ) || !isDirectory( d_.path() ) ) { + throw std::runtime_error( + std::string( "Directory " ) + d_.path() + + " either doesn't exist or isn't a directory" ); + } + current_entry = readdir( d ); // skip "." and ".." @@ -56,6 +63,27 @@ bool FSLib::rename( const string &file_a, const string &file_b ) { return ::rename( file_a.c_str(), file_b.c_str() ) == 0; } +bool FSLib::createDirectoryFull( const string &path ) { + uint64_t pos{}; + // go through all directories leading to the last one + // and create them if they don't exist + do { + // get partial directory path + pos = path.find_first_of( "/", pos ); + if ( pos > 0 ) { + auto dirname = path.substr( 0, pos ); + // create it if it doesn't exist + if ( !FSLib::exists( dirname ) ) { + if ( mkdir( dirname.c_str(), + S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH ) != 0 ) + return false; + } + } + pos++; + } while ( pos < path.length() && pos != 0 ); + return true; +} + FSLib::Directory::iterator FSLib::Directory::end() { return Iterator( *this, nullptr ); } diff --git a/windows/filesystem.cpp b/windows/filesystem.cpp index b30a33f..43d37f2 100644 --- a/windows/filesystem.cpp +++ b/windows/filesystem.cpp @@ -1,6 +1,7 @@ #include "../filesystem.hpp" #include #include +#include #include FSLib::Directory::Directory( const string &path_ ) : dir_path( path_ ) { @@ -9,6 +10,10 @@ FSLib::Directory::Directory( const string &path_ ) : dir_path( path_ ) { } FSLib::Directory::Iterator::Iterator( const Directory &d_ ) { + if ( !exists( d_.path() ) || !isDirectory( d_.path() ) ) { + throw std::runtime_error( + "Directory either doesn't exist or isn't a directory" ); + } hFind = FindFirstFileW( d_.path(), &data ); if ( hFind != INVALID_HANDLE_VALUE ) { if ( !wcscmp( data.cFileName, L"." ) || @@ -75,6 +80,26 @@ bool FSLib::rename( const string &file_a, const string &file_b ) { MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING ); } +bool FSLib::createDirectoryFull( const string &path ) { + uint64_t pos = path.find_first_of( L":", 0 ) + 2; + if ( pos == string::npos ) + pos = 0; + // go through all directories leading to the last one + // and create them if they don't exist + do { + // get partial directory path + pos = path.find_first_of( L"\\", pos ); + auto dirname = path.substr( 0, pos ); + // create it if it doesn't exist + if ( !FSLib::exists( dirname ) ) { + if ( !CreateDirectoryW( dirname.c_str(), NULL ) ) + return false; + } + pos++; + } while ( pos < path.length() && pos != 0 ); + return true; +} + FSLib::Directory::iterator FSLib::Directory::end() { return Iterator( true ); }