From a5f198586b6cd386478a610584271d843e906124 Mon Sep 17 00:00:00 2001 From: CrawfordCurrie Date: Sun, 11 Sep 2011 08:28:10 +0000 Subject: [PATCH] Item11091: Item10961: Item10993: handling of missing ,v files revised, with the store now behaving correctly if a .txt,v is missing or inconsistent with .txt. Note that this *will not work* if used with a file system that does not maintain file modification times, or a perl that doesn't report them correctly, or an RCS that assigns a more recently file time to the .txt than that on the .txt,v. I also have some conserns about performance, but based on my benchmarking can't nail anything down. git-svn-id: http://svn.foswiki.org/branches/Release01x01@12497 0b4bb1d4-4e5a-0410-9cc4-b2b747904278 --- .../Foswiki/Contrib/UnitTestContrib/MANIFEST | 6 +- UnitTestContrib/test/unit/ConfigureTests.pm | 50 +-- UnitTestContrib/test/unit/Fn_SEARCH.pm | 11 +- .../test/unit/FoswikiStoreTestCase.pm | 73 ++++ UnitTestContrib/test/unit/FuncTests.pm | 3 +- UnitTestContrib/test/unit/LoadedRevTests.pm | 4 +- UnitTestContrib/test/unit/MetaTests.pm | 2 +- UnitTestContrib/test/unit/QueryTests.pm | 34 -- .../unit/{RcsTests.pm => RCSHandlerTests.pm} | 68 ++-- UnitTestContrib/test/unit/StoreTests.pm | 194 ++++++---- .../{StoreSmokeTests.pm => VCMetaTests.pm} | 50 +-- UnitTestContrib/test/unit/VCStoreTests.pm | 363 ++++++++++++++++++ .../TestCases/TestCaseAutoFormattedSearch.txt | 4 +- core/lib/Foswiki/Form/Select.pm | 2 +- core/lib/Foswiki/Meta.pm | 15 + core/lib/Foswiki/Render.pm | 3 +- core/lib/Foswiki/Store.pm | 6 +- core/lib/Foswiki/Store/VC/Handler.pm | 280 +++++++++----- core/lib/Foswiki/Store/VC/RcsLiteHandler.pm | 58 ++- core/lib/Foswiki/Store/VC/RcsWrapHandler.pm | 125 +++--- core/lib/Foswiki/Store/VC/Store.pm | 32 +- 21 files changed, 949 insertions(+), 434 deletions(-) create mode 100644 UnitTestContrib/test/unit/FoswikiStoreTestCase.pm rename UnitTestContrib/test/unit/{RcsTests.pm => RCSHandlerTests.pm} (93%) rename UnitTestContrib/test/unit/{StoreSmokeTests.pm => VCMetaTests.pm} (86%) create mode 100644 UnitTestContrib/test/unit/VCStoreTests.pm diff --git a/UnitTestContrib/lib/Foswiki/Contrib/UnitTestContrib/MANIFEST b/UnitTestContrib/lib/Foswiki/Contrib/UnitTestContrib/MANIFEST index 652c472e6c..a8ed478a59 100644 --- a/UnitTestContrib/lib/Foswiki/Contrib/UnitTestContrib/MANIFEST +++ b/UnitTestContrib/lib/Foswiki/Contrib/UnitTestContrib/MANIFEST @@ -69,7 +69,7 @@ test/unit/PluginHandlerTests.pm 0644 test/unit/PrefsTests.pm 0644 test/unit/QueryTests.pm 0644 test/unit/RESTTests.pm 0644 -test/unit/RcsTests.pm 0644 +test/unit/RCSHandlerTests.pm 0644 test/unit/RegisterTests.pm 0644 test/unit/RenameTests.pm 0755 test/unit/RenderFormTests.pm 0644 @@ -80,10 +80,12 @@ test/unit/RobustnessTests.pm 0644 test/unit/SaveScriptTests.pm 0644 test/unit/SeleniumConfigTests.pm 0644 test/unit/SemiAutomaticTestCaseTests.pm 0644 -test/unit/StoreSmokeTests.pm 0644 +test/unit/VCStoreTests.pm 0644 +test/unit/VCMetaTests.pm 0644 test/unit/StoreTests.pm 0644 test/unit/TOCTests.pm 0644 test/unit/FoswikiFnTestCase.pm 0644 +test/unit/FoswikiStoreTestCase.pm 0644 test/unit/FoswikiSeleniumTestCase.pm 0644 test/unit/FoswikiSuite.pm 0644 test/unit/FoswikiTestCase.pm 0644 diff --git a/UnitTestContrib/test/unit/ConfigureTests.pm b/UnitTestContrib/test/unit/ConfigureTests.pm index 398cd73fcc..4b8f24a17f 100644 --- a/UnitTestContrib/test/unit/ConfigureTests.pm +++ b/UnitTestContrib/test/unit/ConfigureTests.pm @@ -1019,16 +1019,16 @@ sub test_Package_makeBackup { $this->assert_matches( qr/Backup saved into/, $msg ); $result = $pkg->uninstall(); - my @expFiles = qw( -Testsandboxweb1234/Subweb/TestTopic43.txt -Testsandboxweb1234/TestTopic1.txt -Testsandboxweb1234/TestTopic43.txt -Testsandboxweb1234/Subweb/TestTopic43/file3.att -Testsandboxweb1234/Subweb/TestTopic43/subdir-1.2.3/file4.att -Testsandboxweb1234/TestTopic1/file.att -Testsandboxweb1234/TestTopic43/file.att -Testsandboxweb1234/TestTopic43/file2.att -configure/pkgdata/MyPlugin_installer + my @expFiles = ( +'Testsandboxweb1234/Subweb/TestTopic43.txt', +'Testsandboxweb1234/TestTopic1.txt', +'Testsandboxweb1234/TestTopic43.txt', +'Testsandboxweb1234/Subweb/TestTopic43/file3.att', +'Testsandboxweb1234/Subweb/TestTopic43/subdir-1.2.3/file4.att', +'Testsandboxweb1234/TestTopic1/file.att', +'Testsandboxweb1234/TestTopic43/file.att', +'Testsandboxweb1234/TestTopic43/file2.att', +'configure/pkgdata/MyPlugin_installer' ); push @expFiles, "$this->{scriptdir}/shbtest1"; @@ -1383,21 +1383,21 @@ qr/^Foswiki::Contrib::OptionalDependency version >=14754 required(.*)^ -- perl m # my $results = $pkg2->uninstall(); - my @expFiles = qw( -Testsandboxweb1234/Subweb/TestTopic43.txt -Testsandboxweb1234/Subweb/TestTopic43.txt,v -Testsandboxweb1234/TestTopic1.txt -Testsandboxweb1234/TestTopic43.txt -Testsandboxweb1234/TestTopic43.txt,v -Testsandboxweb1234/Subweb/TestTopic43/file3.att -Testsandboxweb1234/Subweb/TestTopic43/file3.att,v -Testsandboxweb1234/Subweb/TestTopic43/subdir-1.2.3/file4.att -Testsandboxweb1234/TestTopic1/file.att -Testsandboxweb1234/TestTopic43/file.att -Testsandboxweb1234/TestTopic43/file.att,v -Testsandboxweb1234/TestTopic43/file2.att -Testsandboxweb1234/TestTopic43/file2.att,v -configure/pkgdata/MyPlugin_installer + my @expFiles = ( +'Testsandboxweb1234/Subweb/TestTopic43.txt', +'Testsandboxweb1234/Subweb/TestTopic43.txt,v', +'Testsandboxweb1234/TestTopic1.txt', +'Testsandboxweb1234/TestTopic43.txt', +'Testsandboxweb1234/TestTopic43.txt,v', +'Testsandboxweb1234/Subweb/TestTopic43/file3.att', +'Testsandboxweb1234/Subweb/TestTopic43/file3.att,v', +'Testsandboxweb1234/Subweb/TestTopic43/subdir-1.2.3/file4.att', +'Testsandboxweb1234/TestTopic1/file.att', +'Testsandboxweb1234/TestTopic43/file.att', +'Testsandboxweb1234/TestTopic43/file.att,v', +'Testsandboxweb1234/TestTopic43/file2.att', +'Testsandboxweb1234/TestTopic43/file2.att,v', +'configure/pkgdata/MyPlugin_installer' ); push @expFiles, "$this->{scriptdir}/shbtest1"; diff --git a/UnitTestContrib/test/unit/Fn_SEARCH.pm b/UnitTestContrib/test/unit/Fn_SEARCH.pm index 8684ab9473..402c9d9579 100644 --- a/UnitTestContrib/test/unit/Fn_SEARCH.pm +++ b/UnitTestContrib/test/unit/Fn_SEARCH.pm @@ -2845,6 +2845,15 @@ GNURF ); #order by modified, limit=2, with groupby=none + my @tops = (); + foreach my $web ($this->{test_web}, "Main", "System", "Sandbox") { + if (Foswiki::Func::topicExists($web, "WebHome")) { + my @i = Foswiki::Func::getRevisionInfo($web, "WebHome"); + push(@tops, [$web, $i[0]]); + } + } + @tops = map { $_->[0] } sort { $a->[1] <=> $b->[1] } @tops; + $result = $this->{test_topicObject}->expandMacros( <assert_equals( "HEADERMain WebHome, Sandbox WebHomeFOOTER\n", + $this->assert_equals( "HEADER$tops[0] WebHome, $tops[1] WebHomeFOOTER\n", $result ); } diff --git a/UnitTestContrib/test/unit/FoswikiStoreTestCase.pm b/UnitTestContrib/test/unit/FoswikiStoreTestCase.pm new file mode 100644 index 0000000000..43206e55ec --- /dev/null +++ b/UnitTestContrib/test/unit/FoswikiStoreTestCase.pm @@ -0,0 +1,73 @@ +package FoswikiStoreTestCase; + +# Specialisation of FoswikiFnTestCase used to perform tests over all +# viable store implementations. +# +# Subclasses are expected to implement set_up_for_verify() +# +use FoswikiFnTestCase; +our @ISA = qw( FoswikiFnTestCase ); + +# Determine if RCS is installed. used in tests for RCS functionality. +our $rcs_installed; +sub rcs_is_installed { + if (!defined($rcs_installed)) { + eval { + `co -V`; # Check to see if we have co + }; + if ( $@ || $? ) { + $rcs_installed = 0; + print STDERR "*** CANNOT RUN RcsWrap TESTS - NO COMPATIBLE co: $@\n"; + } else { + $rcs_installed = 1; + } + } + return $rcs_installed; +} + +sub set_up { + my $this = shift; + + $this->SUPER::set_up(); +} + +sub tear_down { + my $this = shift; + + $this->SUPER::tear_down(); +} + +sub set_up_for_verify { + die "ABSTRACT BASE CLASS"; +} + +sub fixture_groups { + my @groups; + foreach my $dir (@INC) { + if ( opendir( D, "$dir/Foswiki/Store" ) ) { + foreach my $alg ( readdir D ) { + next unless $alg =~ s/^(.*)\.pm$/$1/; + next if defined &$alg; + $ENV{PATH} =~ /^(.*)$/ms; + $ENV{PATH} = $1; + next if $alg =~ /RcsWrap/ && !rcs_is_installed(); + ($alg) = $alg =~ /^(.*)$/ms; + eval "require Foswiki::Store::$alg"; + die $@ if $@; + no strict 'refs'; + *$alg = sub { + my $this = shift; + $Foswiki::cfg{Store}{Implementation} = + 'Foswiki::Store::'.$alg; + $this->set_up_for_verify(); + }; + use strict 'refs'; + push(@groups, $alg); + } + closedir(D); + } + } + return \@groups; +} + +1; diff --git a/UnitTestContrib/test/unit/FuncTests.pm b/UnitTestContrib/test/unit/FuncTests.pm index 6ad2a21027..0d867fb5ea 100644 --- a/UnitTestContrib/test/unit/FuncTests.pm +++ b/UnitTestContrib/test/unit/FuncTests.pm @@ -2035,8 +2035,7 @@ sub test_getRevisionAtTime { forcedate => $t2 } ); - $this->assert_equals( - 0, + $this->assert_null( Foswiki::Func::getRevisionAtTime( $this->{test_web}, "ShutThatDoor", $t1 - 60 ) diff --git a/UnitTestContrib/test/unit/LoadedRevTests.pm b/UnitTestContrib/test/unit/LoadedRevTests.pm index 53eb333093..ef8625db22 100644 --- a/UnitTestContrib/test/unit/LoadedRevTests.pm +++ b/UnitTestContrib/test/unit/LoadedRevTests.pm @@ -124,7 +124,7 @@ WHEE my $topicObject = Foswiki::Meta->load( $this->{session}, $this->{test_web}, "NoCommaV"); - $this->assert_equals(3, $topicObject->getLoadedRev()); + $this->assert_equals(1, $topicObject->getLoadedRev()); $topicObject = Foswiki::Meta->new( @@ -145,7 +145,7 @@ WHEE $topicObject = Foswiki::Meta->new( $this->{session}, $this->{test_web}, "NoCommaV"); $topicObject->load(); - $this->assert_equals(3, $topicObject->getLoadedRev()); + $this->assert_equals(1, $topicObject->getLoadedRev()); # Reload 0 $topicObject = Foswiki::Meta->new( diff --git a/UnitTestContrib/test/unit/MetaTests.pm b/UnitTestContrib/test/unit/MetaTests.pm index 065727c367..ebafdcf462 100644 --- a/UnitTestContrib/test/unit/MetaTests.pm +++ b/UnitTestContrib/test/unit/MetaTests.pm @@ -967,7 +967,7 @@ HERE $ti = $meta->getRevisionInfo(); $this->assert_equals('BaseUserMapping_666', $ti->{author}); - $this->assert_equals(1, $ti->{version}); + $this->assert_equals(0, $ti->{version}); $this->assert_equals(0, $ti->{date}); } diff --git a/UnitTestContrib/test/unit/QueryTests.pm b/UnitTestContrib/test/unit/QueryTests.pm index e14baa45f4..961117d0da 100644 --- a/UnitTestContrib/test/unit/QueryTests.pm +++ b/UnitTestContrib/test/unit/QueryTests.pm @@ -369,40 +369,6 @@ sub verify_d2n { $this->check( "d2n notatime", eval => undef ); } -sub verify_string_bops { - my $this = shift; - $this->check( "string='String'", eval => 1 ); - $this->check( "string='String '", eval => 0 ); - $this->check( "string~'String '", eval => 0 ); - $this->check( "string~notafield", eval => 0 ); - $this->check( "notafield=~'SomeTextToTestFor'", eval => 0 ); - $this->check( "string!=notafield", eval => 1 ); - $this->check( "string=notafield", eval => 0 ); - $this->check( "string='Str'", eval => 0 ); - $this->check( "string~'?trin?'", eval => 1 ); - $this->check( "string~'*'", eval => 1 ); - $this->check( "string~'*String'", eval => 1 ); - $this->check( "string~'*trin*'", eval => 1 ); - $this->check( "string~'*in?'", eval => 1 ); - $this->check( "string~'*ri?'", eval => 0 ); - $this->check( "string~'??????'", eval => 1 ); - $this->check( "string~'???????'", eval => 0 ); - $this->check( "string~'?????'", eval => 0 ); - $this->check( "'SomeTextToTestFor'~'Text'", eval => 0, simpler => 0 ); - $this->check( "'SomeTextToTestFor'~'*Text'", eval => 0, simpler => 0 ); - $this->check( "'SomeTextToTestFor'~'Text*'", eval => 0, simpler => 0 ); - $this->check( "'SomeTextToTestFor'~'*Text*'", eval => 1, simpler => 1 ); - $this->check( "string!='Str'", eval => 1 ); - $this->check( "string!='String '", eval => 1 ); - $this->check( "string!='String'", eval => 0 ); - $this->check( "string!='string'", eval => 1 ); - $this->check( "string='string'", eval => 0 ); - $this->check( "macro='\%RED\%'", eval => 1, syntaxOnly => 1 ); - $this->check( "macro~'\%RED?'", eval => 1, syntaxOnly => 1 ); - $this->check( "macro~'?RED\%'", eval => 1, syntaxOnly => 1 ); - $this->check( "macro~'?RED\%'", eval => 1, syntaxOnly => 1 ); -} - sub verify_constants { my $this = shift; $this->check( "undefined", eval => undef ); diff --git a/UnitTestContrib/test/unit/RcsTests.pm b/UnitTestContrib/test/unit/RCSHandlerTests.pm similarity index 93% rename from UnitTestContrib/test/unit/RcsTests.pm rename to UnitTestContrib/test/unit/RCSHandlerTests.pm index 735ccbe2e2..d85b05c997 100644 --- a/UnitTestContrib/test/unit/RcsTests.pm +++ b/UnitTestContrib/test/unit/RCSHandlerTests.pm @@ -1,39 +1,26 @@ -require 5.006; +# Tests for low-level RCS handler code. Store::VC::Store creates a +# transitory handler object for each store item. The handler +# behaviour is only exposed to Store::VC::Store, which is in turn tested +# in VCStoreTests. -package RcsTests; +package RCSHandlerTests; + +use strict; use FoswikiTestCase; our @ISA = qw( FoswikiTestCase ); -use strict; sub new { my $self = shift()->SUPER::new(@_); return $self; } -#TODO: extract this so as can re-use it in other places where Store choices might be made -my $rcs_installed; -sub rcs_is_installed { - if (!defined($rcs_installed)) { - #TODO: erm, who said it needed to be on the PATH? - eval { - `co -V`; # Check to see if we have co - }; - if ( $@ || $? ) { - $rcs_installed = 0; - print STDERR "*** CANNOT RUN RcsWrap TESTS - NO COMPATIBLE co: $@\n"; - } else { - $rcs_installed = 1; - } - } - return $rcs_installed; -} - use Foswiki; use Foswiki::Store; use Foswiki::Store::VC::RcsLiteHandler; use Foswiki::Store::VC::RcsWrapHandler; use File::Path; +use FoswikiStoreTestCase (); my $testWeb = "TestRcsWebTests"; my $user = "TestUser1"; @@ -43,6 +30,7 @@ my $class; my $time = time(); my @historyItem945 = ( + # rcsType, text, comment, user, date [ "Wrap", "old\nwrap\n", "one", "iron", $time ], [ "Wrap", "old\nwrap\nnew\n", "two", "tin", $time + 1 ], [ "Lite", "new\nwrap\nnew\n\nlite\n", "tre", "zinc", $time + 2 ], @@ -64,9 +52,10 @@ sub RcsWrap { } sub fixture_groups { + my $this = shift; my $groups = ['RcsLite']; - push( @$groups, 'RcsWrap' ) if (rcs_is_installed()); + push( @$groups, 'RcsWrap' ) if (FoswikiStoreTestCase::rcs_is_installed()); return ($groups); } @@ -135,22 +124,22 @@ sub verify_RepRev { "in once", "JohnTalintyre" ); my ($text) = $rcs->getRevision(1); $this->assert_equals( "there was a man\n\n", $text ); - $this->assert_equals( 1, $rcs->numRevisions() ); + $this->assert_equals( 1, $rcs->_numRevisions() ); $rcs->replaceRevision( "there was a cat\n", "1st replace", "NotJohnTalintyre", time() ); - $this->assert_equals( 1, $rcs->numRevisions() ); + $this->assert_equals( 1, $rcs->_numRevisions() ); ($text) = $rcs->getRevision(1); $this->assert_equals( "there was a cat\n", $text ); $rcs->addRevisionFromText( "and now this\n\n\n", "2nd entry", "J1" ); - $this->assert_equals( 2, $rcs->numRevisions() ); + $this->assert_equals( 2, $rcs->_numRevisions() ); ($text) = $rcs->getRevision(1); $this->assert_equals( "there was a cat\n", $text ); ($text) = $rcs->getRevision(2); $this->assert_equals( "and now this\n\n\n", $text ); $rcs->replaceRevision( "then this", "2nd replace", "J2", time() ); - $this->assert_equals( 2, $rcs->numRevisions ); + $this->assert_equals( 2, $rcs->_numRevisions ); ($text) = $rcs->getRevision(1); $this->assert_equals( "there was a cat\n", $text ); ($text) = $rcs->getRevision(2); @@ -165,22 +154,22 @@ sub verify_RepRev2839 { $rcs->addRevisionFromText( "there was a man", "in once", "JohnTalintyre" ); my ($text) = $rcs->getRevision(1); $this->assert_equals( "there was a man", $text ); - $this->assert_equals( 1, $rcs->numRevisions() ); + $this->assert_equals( 1, $rcs->_numRevisions() ); $rcs->replaceRevision( "there was a cat", "1st replace", "NotJohnTalintyre", time() ); - $this->assert_equals( 1, $rcs->numRevisions() ); + $this->assert_equals( 1, $rcs->_numRevisions() ); ($text) = $rcs->getRevision(1); $this->assert_equals( "there was a cat", $text ); $rcs->addRevisionFromText( "and now this", "2nd entry", "J1" ); - $this->assert_equals( 2, $rcs->numRevisions() ); + $this->assert_equals( 2, $rcs->_numRevisions() ); ($text) = $rcs->getRevision(1); $this->assert_equals( "there was a cat", $text ); ($text) = $rcs->getRevision(2); $this->assert_equals( "and now this", $text ); $rcs->replaceRevision( "then this", "2nd replace", "J2", time() ); - $this->assert_equals( 2, $rcs->numRevisions ); + $this->assert_equals( 2, $rcs->_numRevisions ); ($text) = $rcs->getRevision(1); $this->assert_equals( "there was a cat", $text ); ($text) = $rcs->getRevision(2); @@ -393,7 +382,7 @@ sub checkGetRevision { $rcs = $class->new( new StoreStub, $testWeb, $topic ); - $this->assert_equals( scalar(@$revs), $rcs->numRevisions() ); + $this->assert_equals( scalar(@$revs), $rcs->_numRevisions() ); for ( my $i = 1 ; $i <= scalar(@$revs) ; $i++ ) { my ($text) = $rcs->getRevision($i); $this->assert_str_equals( $revs->[ $i - 1 ], @@ -519,6 +508,7 @@ sub verify_RevInfo { my ($this) = @_; my $rcs = $class->new( new StoreStub, $testWeb, 'RevInfo', "" ); + $rcs->addRevisionFromText( "Rev1\n", 'FirstComment', "FirstUser", 0 ); $rcs->addRevisionFromText( "Rev2\n", 'SecondComment', "SecondUser", 1000 ); $rcs->addRevisionFromText( "Rev3\n", 'ThirdComment', "ThirdUser", 2000 ); @@ -562,10 +552,10 @@ sub verify_RevInfo { $this->assert_equals( 1, $info->{version} ); $this->assert_str_equals( - $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID, + $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID, $info->{author} ); - $this->assert_str_equals( '', $info->{comment} ); + $this->assert_str_equals( 'pending', $info->{comment} ); } # If a .txt file exists with no ,v and we perform an op on that @@ -582,7 +572,7 @@ sub verify_MissingVrestoreRev { my $rcs = $class->new( new StoreStub, $testWeb, 'MissingV', "" ); my $info = $rcs->getInfo(3); $this->assert_equals( 1, $info->{version} ); - $this->assert_equals( 1, $rcs->numRevisions() ); + $this->assert_equals( 1, $rcs->_numRevisions() ); my ($text) = $rcs->getRevision(0); $this->assert_matches( qr/^Rev 1/, $text ); @@ -615,7 +605,7 @@ sub verify_MissingVrepRev { my $rcs = $class->new( new StoreStub, $testWeb, 'MissingV', "" ); my $info = $rcs->getInfo(3); $this->assert_equals( 1, $info->{version} ); - $this->assert_equals( 1, $rcs->numRevisions() ); + $this->assert_equals( 1, $rcs->_numRevisions() ); my ($text) = $rcs->getRevision(0); $this->assert_matches( qr/^Rev 1/, $text ); @@ -646,7 +636,7 @@ sub verify_MissingVdelRev { my $rcs = $class->new( new StoreStub, $testWeb, 'MissingV', "" ); my $info = $rcs->getInfo(3); $this->assert_equals( 1, $info->{version} ); - $this->assert_equals( 1, $rcs->numRevisions() ); + $this->assert_equals( 1, $rcs->_numRevisions() ); my ($text) = $rcs->getRevision(0); $this->assert_matches( qr/^Rev 1/, $text ); @@ -756,7 +746,7 @@ sub verify_Item3122 { sub test_Item945 { my ($this) = @_; - if (!rcs_is_installed()) { + if (!FoswikiStoreTestCase::rcs_is_installed()) { $this->expect_failure(); $this->annotate("rcs not installed"); } @@ -785,7 +775,7 @@ sub item945_checkHistory { sub item945_checkHistoryRcs { my ( $this, $rcs, $depth ) = @_; - $this->assert_equals( $depth, $rcs->numRevisions() ); + $this->assert_equals( $depth, $rcs->_numRevisions() ); for my $digger ( 1 .. $depth ) { my $info = $historyItem945[ $digger - 1 ]; my $rinfo = $rcs->getInfo($digger); @@ -816,7 +806,7 @@ sub item945_fillTopic { sub test_Item945_diff { my ($this) = @_; - if (!rcs_is_installed()) { + if (!FoswikiStoreTestCase::rcs_is_installed()) { $this->expect_failure(); $this->annotate("rcs not installed"); } diff --git a/UnitTestContrib/test/unit/StoreTests.pm b/UnitTestContrib/test/unit/StoreTests.pm index 4a97964fed..fb1797a2d0 100644 --- a/UnitTestContrib/test/unit/StoreTests.pm +++ b/UnitTestContrib/test/unit/StoreTests.pm @@ -1,10 +1,16 @@ -# Copyright (C) 2005 Sven Dowideit & Crawford Currie +# Copyright (C) 2005-2011 Sven Dowideit & Crawford Currie +# +# Tests for the Foswiki::Store API used by the Foswiki::Meta class to +# interact with the store. +# +# These tests must be independent of the actual store implementation. + require 5.006; package StoreTests; -use FoswikiFnTestCase; -our @ISA = qw( FoswikiFnTestCase ); +use FoswikiStoreTestCase; +our @ISA = qw( FoswikiStoreTestCase ); use Foswiki; use strict; @@ -13,8 +19,6 @@ use Error qw( :try ); use Foswiki::AccessControlException; use File::Temp; -#Test the upper level Store API - #TODO # attachments # check meta data for correctness @@ -24,6 +28,7 @@ use File::Temp; # streams # web creation with options for WebPreferences # search +# getRevisionAtTime sub new { my $self = shift()->SUPER::new(@_); @@ -49,7 +54,6 @@ sub set_up { open( FILE, ">$Foswiki::cfg{TempfileDir}/testfile.gif" ); print FILE "one two three"; close(FILE); - } sub tear_down { @@ -59,13 +63,17 @@ sub tear_down { if ( Foswiki::Func::webExists($web) ); unlink("$Foswiki::cfg{TempfileDir}/testfile.gif"); - #$this->{session}->finish(); $this->SUPER::tear_down(); } +sub set_up_for_verify { + # Required to satisfy superclass +} + #============================================================================ -# tests -sub test_CreateEmptyWeb { +# Create an empty web. There is no template web, so it should be populated with +# a dummy WebPreferences and nothing else. +sub verify_CreateEmptyWeb { my $this = shift; #create an empty web @@ -73,16 +81,17 @@ sub test_CreateEmptyWeb { $webObject->populateNewWeb(); $this->assert( $this->{session}->webExists($web) ); my @topics = $webObject->eachTopic()->all(); - $this->assert_equals( 1, scalar(@topics), join( " ", @topics ) ) - ; #we expect there to be only the home topic + my $tops = join( " ", @topics ); + $this->assert_equals( 1, scalar(@topics), $tops ) + ; #we expect there to be only the preferences topic + $this->assert_equals($Foswiki::cfg{WebPrefsTopicName}, $tops); $webObject->removeFromStore(); } -sub test_CreateWeb { +# Create a web using _default template +sub verify_CreateWeb { my $this = shift; -#create a web using _default -#TODO how should this fail if we are testing a store impl that does not have a _deault web ? my $webObject = Foswiki::Meta->new( $this->{session}, $web ); $webObject->populateNewWeb( '_default', { WEBBGCOLOR => '#123432', SITEMAPLIST => 'on' } ); @@ -99,11 +108,11 @@ sub test_CreateWeb { join( ",", @topics ) . " != " . join( ',', @defaultTopics ) ); } -sub test_CreateWebWithNonExistantBaseWeb { +# Create a web using non-existent Web - it should not create the web +sub verify_CreateWebWithNonExistantBaseWeb { my $this = shift; my $web = 'FailToCreate'; - #create a web using non-existent Web my $ok = 0; try { Foswiki::Func::createWeb( $web, 'DoesNotExists' ); @@ -115,7 +124,8 @@ sub test_CreateWebWithNonExistantBaseWeb { $this->assert( !$this->{session}->webExists($web) ); } -sub test_CreateSimpleTextTopic { +# Create a simple topic containing only text +sub verify_CreateSimpleTextTopic { my $this = shift; Foswiki::Func::createWeb( $web, '_default' ); @@ -132,7 +142,8 @@ sub test_CreateSimpleTextTopic { $webObject->removeFromStore(); } -sub test_CreateSimpleMetaTopic { +# Create a simple topic containing meta-data +sub verify_CreateSimpleMetaTopic { my $this = shift; Foswiki::Func::createWeb( $web, '_default' ); @@ -159,93 +170,119 @@ sub test_CreateSimpleMetaTopic { $webObject->removeFromStore(); } -sub test_getRevisionInfo { +# Save a second version of a topic, without forcing a new revision. Should +# re-use the existing rev number. Stores don't actually need to support this, +# but we currently have no way of interrogating a store for it's capabilities. +sub verify_noForceRev_RepRev { my $this = shift; Foswiki::Func::createWeb( $web, '_default' ); - $this->assert( $this->{session}->webExists($web) ); + $this->assert( !$this->{session}->topicExists( $web, $topic ) ); + + my ( $date, $user, $rev, $comment ); + + ( $date, $user, $rev, $comment ) = + Foswiki::Func::getRevisionInfo( $web, $topic ); + + $this->assert_num_equals( 0, $rev ); # topic does not exist + my $text = "This is some test text\n * some list\n * content\n :) :)"; my $meta = Foswiki::Meta->new( $this->{session}, $web, $topic, $text ); $meta->save(); - $this->assert_equals( 1, $meta->getLatestRev() ); - - $text .= "\nnewline"; - $meta->text($text); - $meta->save( forcenewrevision => 1 ); + $this->assert( $this->{session}->topicExists( $web, $topic ) ); + ( $date, $user, $rev, $comment ) = + Foswiki::Func::getRevisionInfo( $web, $topic ); + $this->assert_num_equals( 1, $rev ); my $readMeta = Foswiki::Meta->load( $this->{session}, $web, $topic ); - my $readText = $readMeta->text; + $this->assert_str_equals( $text, $readMeta->text ); - # ignore whitspace at end of data - $readText =~ s/\s*$//s; - $this->assert_equals( $text, $readText ); - $this->assert_equals( 2, $readMeta->getLatestRev() ); - my $info = $readMeta->getRevisionInfo(); - $this->assert_str_equals( $this->{session}->{user}, $info->{author} ); - $this->assert_num_equals( 2, $info->{version} ); + $text = "new text"; + $meta->text($text); + $meta->save(); + $this->assert( $this->{session}->topicExists( $web, $topic ) ); + ( $date, $user, $rev, $comment ) = + Foswiki::Func::getRevisionInfo( $web, $topic ); + $this->assert_num_equals( 1, $rev ); - #TODO - #getRevisionDiff ( $web, $topic, $rev1, $rev2, $contextLines ) -> \@diffArray + #cleanup my $webObject = Foswiki::Meta->new( $this->{session}, $web ); $webObject->removeFromStore(); } -sub test_getRevisionInfoNoRcsFile { +# Save a topic, forcing a new revision. Should increment the rev number. +sub verify_ForceRev { my $this = shift; Foswiki::Func::createWeb( $web, '_default' ); $this->assert( $this->{session}->webExists($web) ); + $this->assert( !$this->{session}->topicExists( $web, $topic ) ); - my $ttext = <assert_num_equals( 0, $rev ); # doesn't exist yet -Edit this topic to add a description to the AdminGroup -DONE + my $text = "This is some test text\n * some list\n * content\n :) :)"; + my $meta = Foswiki::Meta->new( $this->{session}, $web, $topic, $text ); + $meta->save( forcenewrevision => 1 ); + $this->assert( $this->{session}->topicExists( $web, $topic ) ); + ( $date, $user, $rev, $comment ) = + Foswiki::Func::getRevisionInfo( $web, $topic ); + $this->assert_num_equals( 1, $rev ); - my $rawtext = <load( $this->{session}, $web, $topic ); + $this->assert_str_equals( $text, $readMeta->text ); - open( my $fh, '>', "$Foswiki::cfg{DataDir}/$web/$topic.txt" ) - || die "Unable to open \n $! \n\n "; - print $fh $rawtext; - close $fh; + $text = "new text"; + $meta->text($text); + $meta->save( forcenewrevision => 1 ); + $this->assert( $this->{session}->topicExists( $web, $topic ) ); + ( $date, $user, $rev, $comment ) = + Foswiki::Func::getRevisionInfo( $web, $topic ); + $this->assert_num_equals( 2, $rev ); - # A file without history should be rev 0, not rev 1. - my $meta = Foswiki::Meta->load( $this->{session}, $web, $topic ); + #cleanup + my $webObject = Foswiki::Meta->new( $this->{session}, $web ); + $webObject->removeFromStore(); +} - #$this->assert_equals( 0, $meta->getLatestRev() ); - $this->assert_str_equals( $ttext, $meta->text() ); +# Get the revision info of the latest rev of the topic. +sub verify_getRevisionInfo { + my $this = shift; - $meta->text( $ttext . "\nnewline" ); + Foswiki::Func::createWeb( $web, '_default' ); -# Save without force revision still should create a new rev due to missing history - $meta->save( forcenewrevision => 0 ); + $this->assert( $this->{session}->webExists($web) ); + my $text = "This is some test text\n * some list\n * content\n :) :)"; + my $meta = Foswiki::Meta->new( $this->{session}, $web, $topic, $text ); + $meta->save(); + $this->assert_equals( 1, $meta->getLatestRev() ); - # Save of a file without an existing RCS file should not modify Rev 1, - # but should instead create the next revision, so rev 1 represents - # the original file before history started. + $text .= "\nnewline"; + $meta->text($text); + $meta->save( forcenewrevision => 1 ); my $readMeta = Foswiki::Meta->load( $this->{session}, $web, $topic ); - $this->assert_str_equals( $ttext . "\nnewline", $readMeta->text() ); + my $readText = $readMeta->text; - $this->assert_equals( 2, $readMeta->getLatestRev() ); + # ignore whitespace at end of data + $readText =~ s/\s*$//s; + $this->assert_equals( $text, $readText ); + $this->assert_equals( 2, $readMeta->getLatestRev() ); my $info = $readMeta->getRevisionInfo(); $this->assert_str_equals( $this->{session}->{user}, $info->{author} ); $this->assert_num_equals( 2, $info->{version} ); - # Make sure that rev 1 exists and has the original text pr-history. - my $oldMeta = Foswiki::Meta->load( $this->{session}, $web, $topic, '1' ); - $this->assert_str_equals( $ttext, $oldMeta->text() ); - + #TODO + #getRevisionDiff ( $web, $topic, $rev1, $rev2, $contextLines ) -> \@diffArray my $webObject = Foswiki::Meta->new( $this->{session}, $web ); $webObject->removeFromStore(); } -sub test_moveTopic { +# Move a topic to another name in the same web +sub verify_moveTopic { my $this = shift; Foswiki::Func::createWeb( $web, '_default' ); @@ -283,7 +320,8 @@ sub test_moveTopic { } -sub test_leases { +# Check that leases are taken, and timed correctly +sub verify_leases { my $this = shift; Foswiki::Func::createWeb( $web, '_default' ); @@ -325,7 +363,8 @@ sub beforeSaveHandler { use Foswiki::Plugin; -sub test_beforeSaveHandlerChangeText { +# Ensure the beforeSaveHandler is called when saving text changes +sub verify_beforeSaveHandlerChangeText { my $this = shift; my $args = { name => "fieldname", @@ -369,7 +408,8 @@ sub test_beforeSaveHandlerChangeText { $webObject->removeFromStore(); } -sub test_beforeSaveHandlerChangeMeta { +# Ensure the beforeSaveHandler is called when saving meta changes +sub verify_beforeSaveHandlerChangeMeta { my $this = shift; my $args = { name => "fieldname", @@ -412,7 +452,8 @@ sub test_beforeSaveHandlerChangeMeta { $webObject->removeFromStore(); } -sub test_beforeSaveHandlerChangeBoth { +# Ensure the beforeSaveHandler is called when saving text and meta changes +sub verify_beforeSaveHandlerChangeBoth { my $this = shift; my $args = { name => "fieldname", @@ -479,6 +520,7 @@ sub beforeUploadHandler { $attrHash->{stream} = $fh; } +# Handler used in next test sub beforeAttachmentSaveHandler { my ( $attrHash, $topic, $web ) = @_; die "attachment $attrHash->{attachment}" @@ -508,6 +550,7 @@ sub afterAttachmentSaveHandler { unless $attrHash->{comment} eq "a comment"; } +# Handler used in next test sub afterUploadHandler { my ( $attrHash, $meta ) = @_; die "attachment $attrHash->{attachment}" @@ -550,7 +593,7 @@ sub registerAttachmentHandlers { ); } -sub test_attachmentSaveHandlers_file { +sub verify_attachmentSaveHandlers_file { my $this = shift; open( FILE, ">$Foswiki::cfg{TempfileDir}/testfile.gif" ); @@ -578,7 +621,7 @@ sub test_attachmentSaveHandlers_file { "beforeAttachmentSaveHandler beforeUploadHandler call", $text ); } -sub test_attachmentSaveHandlers_stream { +sub verify_attachmentSaveHandlers_stream { my $this = shift; open( FILE, ">$Foswiki::cfg{TempfileDir}/testfile.gif" ); @@ -607,7 +650,7 @@ sub test_attachmentSaveHandlers_stream { "beforeAttachmentSaveHandler beforeUploadHandler call", $text ); } -sub test_attachmentSaveHandlers_file_and_stream { +sub verify_attachmentSaveHandlers_file_and_stream { my $this = shift; open( FILE, ">$Foswiki::cfg{TempfileDir}/testfile.gif" ); @@ -637,7 +680,7 @@ sub test_attachmentSaveHandlers_file_and_stream { "beforeAttachmentSaveHandler beforeUploadHandler call", $text ); } -sub test_eachChange { +sub verify_eachChange { my $this = shift; Foswiki::Func::createWeb($web); $Foswiki::cfg{Store}{RememberChangesFor} = 5; # very bad memory @@ -687,7 +730,7 @@ sub test_eachChange { $this->assert( !$it->hasNext() ); } -sub test_eachAttachment { +sub verify_eachAttachment { my $this = shift; my $meta = @@ -764,7 +807,6 @@ sub test_eachAttachment { $it = $this->{session}->{store}->eachAttachment($postDeleteMeta); $list = join( ' ', sort $it->all() ); $this->assert_str_equals( "noise.dat", $list ); - } 1; diff --git a/UnitTestContrib/test/unit/StoreSmokeTests.pm b/UnitTestContrib/test/unit/VCMetaTests.pm similarity index 86% rename from UnitTestContrib/test/unit/StoreSmokeTests.pm rename to UnitTestContrib/test/unit/VCMetaTests.pm index 2b6a0d6fcc..669a974969 100644 --- a/UnitTestContrib/test/unit/StoreSmokeTests.pm +++ b/UnitTestContrib/test/unit/VCMetaTests.pm @@ -1,8 +1,12 @@ -# Smoke tests for Foswiki::Store -package StoreSmokeTests; +# These tests cycle through the available store implementations +# and check that calls to the methods of Foswiki::Meta respond +# approrpiately. The tests are cursory, and only intended as a +# "sanity check" of store functionality. More complete tests of +# VC functionality can be found in VCStoreTests and VCHandlerTests. +package VCMetaTests; -use FoswikiFnTestCase; -our @ISA = qw( FoswikiFnTestCase ); +use FoswikiStoreTestCase; +our @ISA = qw( FoswikiStoreTestCase ); use strict; use Foswiki; @@ -15,43 +19,6 @@ my $testUser1; my $testUser2; my $UI_FN; -sub fixture_groups { - my @groups; - foreach my $dir (@INC) { - if ( opendir( D, "$dir/Foswiki/Store" ) ) { - foreach my $alg ( readdir D ) { - next unless $alg =~ s/^(.*)\.pm$/$1/; - next if defined &$alg; - $ENV{PATH} =~ /^(.*)$/ms; - $ENV{PATH} = $1; - if ($alg =~ /RcsWrap/) { - eval { - `co -V`; # Check to see if we have co - }; - if ( $@ || $? ) { - print STDERR "*** CANNOT RUN RcsWrap TESTS - NO COMPATIBLE co: $@\n"; - next; - } - } - ($alg) = $alg =~ /^(.*)$/ms; - eval "require Foswiki::Store::$alg"; - die $@ if $@; - no strict 'refs'; - *$alg = sub { - my $this = shift; - $Foswiki::cfg{Store}{Implementation} = - 'Foswiki::Store::'.$alg; - $this->set_up_for_verify(); - }; - use strict 'refs'; - push(@groups, $alg); - } - closedir(D); - } - } - return \@groups; -} - # Set up the test fixture sub set_up_for_verify { my $this = shift; @@ -61,6 +28,7 @@ sub set_up_for_verify { $Foswiki::cfg{WarningFileName} = "$Foswiki::cfg{TempfileDir}/junk"; $Foswiki::cfg{LogFileName} = "$Foswiki::cfg{TempfileDir}/junk"; + $this->{session}->finish(); $this->{session} = new Foswiki(); $testUser1 = "DummyUserOne"; diff --git a/UnitTestContrib/test/unit/VCStoreTests.pm b/UnitTestContrib/test/unit/VCStoreTests.pm new file mode 100644 index 0000000000..0f7525c30d --- /dev/null +++ b/UnitTestContrib/test/unit/VCStoreTests.pm @@ -0,0 +1,363 @@ +# Copyright (C) 2011 Foswiki Contributors. All rights reserved. +# +# Tests specific to VC stores, where the history is decoupled from +# the latest rev of the topic. At present that means RCS stores. +# These tests enhance those in StoreTests.pm, but do not replace them. +# +# A VC-controlled file may be in one of four possible states: +# +# "up to date" means the .txt and .txt,v both exist, and are consistent +# "inconsistent" means the .txt is newer than the .txt,v +# "no history" means the .txt exists but there is no .txt,v +# +# Tests for the store in "up to date" state are covered in StoreTests and +# do not need to be repeated here. The tests here are specific to the +# "inconsistent" and "no history" states. + +# Coverage: +# readTopic no history - verify_NoHistory_getRevisionInfo +# readTopic inconsistent - verify_InconsistentTopic_getRevisionInfo +# getRevisionHistory no history - verify_NoHistory_getRevisionInfo +# getRevisionHistory inconsistent - verify_InconsistentTopic_getRevisionInfo +# getNextRevision no history - verify_NoHistory_getRevisionInfo +# getNextRevision inconsistent - verify_InconsistentTopic_getRevisionInfo +# saveTopic no history - verify_NoHistory_implicitSave +# saveTopic inconsistent - verify_Inconsistent_implicitSave +# repRev no history - verify_NoHistory_repRev +# repRev inconsistent - verify_Inconsistent_repRev +# getVersionInfo no history - verify_NoHistory_getRevisionInfo +# getVersionInfo inconsistent - verify_InconsistentTopic_getRevisionInfo +# getRevisionAtTime no history - verify_NoHistory_getRevisionAtTime +# getRevisionAtTime inconsistent - verify_Inconsistent_getRevisionAtTime +# saveAttachment no history +# saveAttachment inconsistent +# getRevisionDiff no history +# getRevisionDiff inconsistent + +package VCStoreTests; + +use FoswikiStoreTestCase; +our @ISA = qw( FoswikiStoreTestCase ); +use strict; + +use Foswiki; +use Foswiki::Meta; + +my $TEXT1 = <<'DONE'; +He had bought a large map representing the sea, +Without the least vestige of land: +And the crew were much pleased when they found it to be +A map they could all understand. +DONE + +my $TEXT2 = <{session}->finish(); + $this->{session} = new Foswiki(); +} + +# private; create a topic with no ,v +sub _createNoHistoryTopic { + my ($this, $noTOPICINFO) = @_; + + $this->{test_topic} .= "NoHistory"; + + open( my $fh, '>', "$Foswiki::cfg{DataDir}/$this->{test_web}/$this->{test_topic}.txt" ) + || die "Unable to open \n $! \n\n "; + print $fh <{test_topic} .= "Inconsistent"; + + my $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $meta->text($TEXT1); + $meta->save(); # we should have a history now, with topic 1 as the latest rev + + # Wait for the clock to tick + my $x = time; + while (time == $x) { + sleep 1; + } + + # create the mauled content + open( my $fh, '>', "$Foswiki::cfg{DataDir}/$this->{test_web}/$this->{test_topic}.txt" ) + || die "Unable to open \n $! \n\n "; + print $fh <_createNoHistoryTopic(); + + # A topic without history should be rev 1 + my $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + # 3 + my $it = $this->{session}->{store}->getRevisionHistory($meta); + $this->assert($it->hasNext()); + $this->assert_num_equals( 1, $it->next() ); + # 1 + $this->assert_matches( qr/^\s*\Q$TEXT1\E\s*$/s, $meta->text() ); +# my $ti = $meta->get('TOPICINFO'); +# $this->assert_num_equals(1, $ti->{version}); +# $this->assert_str_equals($Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID, $ti->{author}); + + # 5 + $this->assert_num_equals(2, $this->{session}->{store}->getNextRevision($meta)); + # 17 + my $info = $this->{session}->{store}->getVersionInfo($meta); + # the TOPICINFO{version} should be ignored if the ,v does not exist, and the rev + # number reverted to 1 + $this->assert_num_equals(1, $info->{version}); + # the author will be reverted to the unknown user + $this->assert_str_equals($Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID, $info->{author}); +} + +sub verify_InconsistentTopic_getRevisionInfo { + my $this = shift; + + # Inconsistent cache with topicinfo + $this->_createInconsistentTopic(); + my $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + # 4 + my $it = $this->{session}->{store}->getRevisionHistory($meta); + $this->assert($it->hasNext()); + $this->assert_num_equals( 2, $it->next() ); + # 6 + $this->assert_num_equals(3, $this->{session}->{store}->getNextRevision($meta)); + + # The content should come from the mauled topic + # 2 + $this->assert_matches( qr/^\s*\Q$TEXT2\E\s*$/s, $meta->text() ); +# my $ti = $meta->get('TOPICINFO'); +# $this->assert_num_equals(2, $ti->{version}); +# $this->assert_str_equals($Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID, $ti->{author}); + my $info = $this->{session}->{store}->getVersionInfo($meta); + $this->assert_num_equals(2, $info->{version}); + $this->assert_str_equals($Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID, $info->{author}); +} + +# A history should be created if none yet exists +sub verify_NoHistory_implicitSave { + my $this = shift; + + $this->_createNoHistoryTopic(); + + # There's no history, but the current .txt is implicit rev 1 + my $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + my $it = $this->{session}->{store}->getRevisionHistory($meta); + $this->assert($it->hasNext()); + $this->assert_num_equals( 1, $it->next() ); + + # Save (but *don't* force) a new rev. + $meta->text( $TEXT2 ); + my $checkSave = $this->{session}->{store}->saveTopic( + $meta, $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID, { comment => "unit test" } ); + + # Save of a file without an existing history should never modify Rev 1, + # but should instead create the first revision, so rev 1 represents + # the original file before history started. + + my $readMeta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $this->assert_equals( 2, $readMeta->getLatestRev() ); + $this->assert_matches( qr/^\s*\Q$TEXT2\E\s*/s, $readMeta->text() ); + + # Check that getRevisionInfo says the right things. The author should be + # retained, but the date and version number should change + my $info = $readMeta->getRevisionInfo(); + $this->assert_str_equals( $this->{session}->{user}, $info->{author} ); + $this->assert_num_equals( 2, $info->{version} ); + + # Make sure that rev 1 exists and has the original text pre-history. + $readMeta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic}, 1 ); + $this->assert_matches( qr/^\s*\Q$TEXT1\E\s*$/s, $readMeta->text() ); +} + +# Save without force revision should create a new rev due to missing history +sub verify_Inconsistent_implicitSave { + my $this = shift; + + $this->_createInconsistentTopic(); + + # Head of "history" will be 2, and should contain $TEXT2 + my $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + + # Save (but *don't* force) a new rev. Should always create a 3. + $meta->text( $TEXT3 ); + my $checkSave = $this->{session}->{store}->saveTopic( + $meta, $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID, { comment => "unit test" } ); + + # Save of a file without an existing history should never modify Rev 1, + # but should instead create the first revision, so rev 1 represents + # the original file before history started. + + my $readMeta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $this->assert_equals( 3, $readMeta->getLatestRev() ); + $this->assert_matches( qr/^\s*\Q$TEXT3\E\s*/s, $readMeta->text() ); + + # Check that getRevisionInfo says the right things. The author should be + # retained, but the date and version number should change + my $info = $readMeta->getRevisionInfo(); + $this->assert_str_equals( $this->{session}->{user}, $info->{author} ); + $this->assert_num_equals( 3, $info->{version} ); + + # Make sure that previous revs exist and have the right content + $readMeta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic}, 1 ); + $this->assert_matches( qr/^\s*\Q$TEXT1\E\s*/s, $readMeta->text() ); + $readMeta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic}, 2 ); + $this->assert_matches( qr/^\s*\Q$TEXT2\E\s*/s, $readMeta->text() ); + $readMeta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic}, 3 ); + $this->assert_matches( qr/^\s*\Q$TEXT3\E\s*/s, $readMeta->text() ); +} + +# repRev a topic that has no existing history. The information passed in the repRev call +# will be used to populate the TOPICINFO of the 'new' revision. +sub verify_NoHistory_repRev { + my $this = shift; + + $this->_createNoHistoryTopic(); + + my $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $meta->text($TEXT2); + # save using a different user (implicit save is done by UNKNOWN user) + $this->{session}->{store}->repRev( $meta, $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID ); + my $info = $this->{session}->{store}->getVersionInfo($meta); + $this->assert_num_equals( 1, $info->{version} ); + $this->assert_str_equals( $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID, $info->{author} ); + $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $this->assert_matches( qr/^\s*\Q$TEXT2\E\s*$/s, $meta->text ); +} + +sub verify_Inconsistent_repRev { + my $this = shift; + + $this->_createInconsistentTopic(); + + my $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $meta->text($TEXT3); + # save using a different user (implicit save is done by UNKNOWN user) + $this->{session}->{store}->repRev( $meta, $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID ); + my $info = $this->{session}->{store}->getVersionInfo($meta); + $this->assert_num_equals( 2, $info->{version} ); + $this->assert_str_equals( $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID, $info->{author} ); + $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $this->assert_matches( qr/^\s*\Q$TEXT3\E\s*$/s, $meta->text ); +} + +sub verify_NoHistory_getRevisionAtTime { + my $this = shift; + + my $then = time; + $this->_createNoHistoryTopic(); + + my $meta = Foswiki::Meta->new( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $this->assert_num_equals(1, $this->{session}->{store}->getRevisionAtTime($meta, time)); + $this->assert_null($this->{session}->{store}->getRevisionAtTime($meta, $then-1)); +} + +# A pending checkin is assumed to have been created at the file modification time of the +# .txt file. +sub verify_Inconsistent_getRevisionAtTime { + my $this = shift; + + my $then = time; + $this->_createInconsistentTopic(); + + my $meta = Foswiki::Meta->new( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $this->assert_num_equals(2, $this->{session}->{store}->getRevisionAtTime($meta, time)); + $this->assert_num_equals(1, $this->{session}->{store}->getRevisionAtTime($meta, $then)); + $this->assert_null($this->{session}->{store}->getRevisionAtTime($meta, $then-1)); +} + +# Note this test uses Foswiki::Meta because it is that module that handles the +# decoration of the topic text with meta-data. Less than ideal, but there you go, this +# is really just a sanity check. +sub verify_NoHistory_saveAttachment { + my $this = shift; + + $this->_createNoHistoryTopic(); + + open( FILE, ">", "$Foswiki::cfg{TempfileDir}/testfile.txt" ); + print FILE "one two three"; + close( FILE ); + + my $meta = Foswiki::Meta->new( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $meta->attach(name => "testfile.txt", + file => "$Foswiki::cfg{TempfileDir}/testfile.txt", + comment => "a comment" ); + + $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $this->assert_equals( 2, $meta->getLatestRev() ); + $this->assert_matches( qr/^\s*\Q$TEXT1\E\s*/s, $meta->text() ); + $this->assert_not_null($meta->get( 'FILEATTACHMENT', 'testfile.txt' )); + + # Check that the new rev has the attachment meta-data + my $info = $meta->getRevisionInfo(); + $this->assert_str_equals( $this->{session}->{user}, $info->{author} ); + $this->assert_num_equals( 2, $info->{version} ); + + # Make sure that rev 1 exists, has the right text + $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic}, 1 ); + $this->assert_matches( qr/^\s*\Q$TEXT1\E\s*$/s, $meta->text() ); +} + +sub verify_Inconsistent_saveAttachment { + my $this = shift; + + $this->_createInconsistentTopic(); + + open( FILE, ">", "$Foswiki::cfg{TempfileDir}/testfile.txt" ); + print FILE "one two three"; + close( FILE ); + + my $meta = Foswiki::Meta->new( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $meta->attach(name => "testfile.txt", + file => "$Foswiki::cfg{TempfileDir}/testfile.txt", + comment => "a comment" ); + + $meta = Foswiki::Meta->load( $this->{session}, $this->{test_web}, $this->{test_topic} ); + $this->assert_equals( 3, $meta->getLatestRev() ); + $this->assert_matches( qr/^\s*\Q$TEXT2\E\s*/s, $meta->text() ); + $this->assert_not_null($meta->get( 'FILEATTACHMENT', 'testfile.txt' )); + + # Check that the new rev has the attachment meta-data + my $info = $meta->getRevisionInfo(); + $this->assert_str_equals( $this->{session}->{user}, $info->{author} ); + $this->assert_num_equals( 3, $info->{version} ); +} + +1; + diff --git a/core/data/TestCases/TestCaseAutoFormattedSearch.txt b/core/data/TestCases/TestCaseAutoFormattedSearch.txt index bfd38afde8..9714ea4133 100755 --- a/core/data/TestCases/TestCaseAutoFormattedSearch.txt +++ b/core/data/TestCases/TestCaseAutoFormattedSearch.txt @@ -82,9 +82,11 @@ Forma... ---+ Formatted singled level search with $rev, $parent and $formname ---++ Expected +_Note that the .txt says it is rev 4, but because there is no ,v to back this up +it is treated as rev 1_ -Revision 4 has parent TestCaseAutoFormattedSearch and it contains the form FormattedSearchForm +Revision 1 has parent TestCaseAutoFormattedSearch and it contains the form FormattedSearchForm ---++ Actual diff --git a/core/lib/Foswiki/Form/Select.pm b/core/lib/Foswiki/Form/Select.pm index 56122f65d4..4b1fa01c8f 100644 --- a/core/lib/Foswiki/Form/Select.pm +++ b/core/lib/Foswiki/Form/Select.pm @@ -56,7 +56,7 @@ sub getOptions { my $str; foreach my $val (@$vals) { if ( $val =~ /^(.*[^\\])*=(.*)$/ ) { - $str = TAINT($1) || ''; + $str = TAINT($1 || ''); $val = $2; $str =~ s/\\=/=/g; } diff --git a/core/lib/Foswiki/Meta.pm b/core/lib/Foswiki/Meta.pm index fc49371afc..c401486143 100644 --- a/core/lib/Foswiki/Meta.pm +++ b/core/lib/Foswiki/Meta.pm @@ -1355,6 +1355,21 @@ sub getRevisionInfo { if DEBUG; my $info; + if ( not defined( $this->{_loadedRev} ) + and not Foswiki::Func::topicExists( $this->{_web}, $this->{_topic} ) ) + { + +#print STDERR "topic does not exist - at least, _loadedRev is not set..(".$this->{_web} .' '. $this->{_topic}.")\n"; +#this does not exist on disk - no reason to goto the store for the defaults +#TODO: Sven is not 100% sure this is the right decision, but it feels better not to do a trip into the deep for an application default + $info = { + date => 0, + author => $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID, + version => 0, + format => $EMBEDDING_FORMAT_VERSION, + }; + return $info; + } # This used to try and get revision info from the meta # information and only kick down to the Store module for the diff --git a/core/lib/Foswiki/Render.pm b/core/lib/Foswiki/Render.pm index 4db54190f3..5757377f4d 100644 --- a/core/lib/Foswiki/Render.pm +++ b/core/lib/Foswiki/Render.pm @@ -943,6 +943,7 @@ sub renderFORMFIELD { # this may have been a one-off optimisation. my $formTopicObject = $this->{ffCache}{ $topicObject->getPath() . $rev }; unless ($formTopicObject) { + undef $rev unless $rev; $formTopicObject = Foswiki::Meta->load( $this->{session}, $topicObject->web, $topicObject->topic, $rev ); @@ -954,7 +955,7 @@ sub renderFORMFIELD { Foswiki::Meta->new( $this->{session}, $topicObject->web, $topicObject->topic, '' ); } - $this->{ffCache}{ $formTopicObject->getPath() . $rev } = + $this->{ffCache}{ $formTopicObject->getPath() . ($rev||0) } = $formTopicObject; } diff --git a/core/lib/Foswiki/Store.pm b/core/lib/Foswiki/Store.pm index f1e45c6de0..4c2a466e22 100644 --- a/core/lib/Foswiki/Store.pm +++ b/core/lib/Foswiki/Store.pm @@ -317,10 +317,14 @@ sub openAttachment { * =$topicObject= - Foswiki::Meta for the topic * =$attachment= - name of an attachment (optional) Get an iterator over the list of revisions of the object. The iterator returns -the revision identifiers (which will usually be numbers) starting with the most recent revision. +the revision identifiers (which will usually be numbers) starting with the most +recent revision. MUST WORK FOR ATTACHMENTS AS WELL AS TOPICS +If the object does not exist, returns an empty iterator ($iterator->hasNext() will be +false). + =cut sub getRevisionHistory { diff --git a/core/lib/Foswiki/Store/VC/Handler.pm b/core/lib/Foswiki/Store/VC/Handler.pm index decc43a975..d8edadcc80 100644 --- a/core/lib/Foswiki/Store/VC/Handler.pm +++ b/core/lib/Foswiki/Store/VC/Handler.pm @@ -204,50 +204,172 @@ sub _controlFileName { Returns info where version is the number of the rev for which the info was recovered, date is the date of that rev (epoch s), user is the canonical user ID of the user who saved that rev, and comment is the comment associated with the rev. Designed to be overridden by subclasses, which can call up to this method -if file-based rev info is required. +if simple file-based rev info is required. =cut sub getInfo { - my $this = shift; + my $this = shift; # $version is not useful here, as we have no way to record history # SMELL: this is only required for the constant require Foswiki::Users::BaseUserMapping; - # If a topic file exists, grab the TOPICINFO from it. This is - # the default behaviour if no implementing class can come up - # with a better answer. Note that we only peek at the first line - # of the file, which is where a "proper" save will have left the tag. + # We only arrive here if the implementation getInfo can't serve the info; this + # will usually be because the ,v is missing or the topic cache is newer. + + # If there is a .txt file, grab the TOPICINFO from it. + # Note that we only peek at the first line of the file, + # which is where a "proper" save will have left the tag. my $info = {}; my $f; - if ( open( $f, '<', $this->{file} ) ) { - local $/ = "\n"; - my $ti = <$f>; - close($f); - if ( defined $ti && $ti =~ /^%META:TOPICINFO{(.*)}%/ ) { - require Foswiki::Attrs; - my $a = Foswiki::Attrs->new($1); - - # Default bad revs to 1, not 0, because this is coming from - # a topic on disk, so we know it's a "real" rev. - $info->{version} = Foswiki::Store::cleanUpRevID( $a->{version} ) - || 1; - $info->{date} = $a->{date} if defined $a->{date}; - $info->{author} = $a->{author} if defined $a->{author}; - $info->{comment} = $a->{comment} if defined $a->{comment}; - } + if ( $this->noCheckinPending() ) { + # TOPICINFO may be OK + if ( open( $f, '<', $this->{file} ) ) { + local $/ = "\n"; + my $ti = <$f>; + close($f); + if ( defined $ti && $ti =~ /^%META:TOPICINFO{(.*)}%/ ) { + require Foswiki::Attrs; + my $a = Foswiki::Attrs->new($1); + + # Default bad revs to 1, not 0, because this is coming from + # a topic on disk, so we know it's a "real" rev. + $info->{version} = Foswiki::Store::cleanUpRevID( $a->{version} ) + || 1; + $info->{date} = $a->{date}; + $info->{author} = $a->{author}; + $info->{comment} = $a->{comment}; + } + } + } else { + # There is a checkin pending. We need the latest rev in the history + 1 + $info->{version} = -e $this->{rcsFile} ? $this->_numRevisions() + 1 : 1; + $info->{comment} = "pending"; } - - # version, date, author and comment fields *must* be defined - $info->{version} = 1 unless defined $info->{version}; $info->{date} = $this->getTimestamp() unless defined $info->{date}; - $info->{author} = $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID - unless defined $info->{author}; - $info->{comment} = '' - unless defined $info->{comment}; + $info->{version} = 1 unless defined $info->{version}; + $info->{comment} = '' unless defined $info->{comment}; + $info->{author} ||= $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID; return $info; } +# Check to see if there is a newer non-,v file waiting to be checked in. If there is, then +# all rev numbers have to be incremented, as they will auto-increment when it is finally +# checked in (usually as the result of a save). This is also used to test the validity of +# TOPICINFO, as a pending checkin does not contain valid TOPICINFO. +sub noCheckinPending { + my $this = shift; + my $isValid = 0; + + if (! -e $this->{file}) { + $isValid = 1; # Hmmmm...... + } else { + if (-e $this->{rcsFile}) { + # Check the time on the rcs file; is the .txt newer? + # Danger, Will Robinson! stat isn't reliable on all file systems, though [9] is claimed to be OK + # See perldoc perlport for more on this. + local ${^WIN32_SLOPPY_STAT} = 1; # don't need to open the file on Win32 + my $rcsTime = (stat($this->{rcsFile}))[9]; + my $fileTime = (stat($this->{file}))[9]; + $isValid = ($rcsTime < $fileTime) ? 0 : 1; + } + } + return $isValid; +} + +# Must be implemented by subclasses +sub ci { + die "Pure virtual method"; +} + +# Protected for use only in subclasses. Check that the object has a history +# and the .txt is consistent with that history. +sub _saveDamage { + my $this = shift; + return if $this->noCheckinPending(); + + # the version in the TOPICINFO may not be correct. We need + # to check the change in and update the TOPICINFO accordingly + my $t = $this->readFile($this->{file}); + + # If this is a topic, adjust the TOPICINFO + if (defined $this->{topic} && !defined $this->{attachment}) { + my $rev = -e $this->{rcsFile} ? $this->getLatestRevisionID() : 1; + $t =~ s/^%META:TOPICINFO{(.*)}%$//m; + $t = '%META:TOPICINFO{author="' + . $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID. + '" comment="autosave" date="' .time().'" format="1.1" version="' + .$rev.'"}%'."\n$t"; + } + $this->ci( 0, + $t, 'autosave', + $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID, time()); +} + +=begin TML + +---++ ObjectMethod addRevisionFromText($text, $comment, $cUID, $date) + +Add new revision. Replace file with text. + * =$text= of new revision + * =$comment= checkin comment + * =$cUID= is a cUID. + * =$date= in epoch seconds; may be ignored + +=cut + +sub addRevisionFromText { + my ( $this, $text, $comment, $user, $date ) = @_; + $this->init(); + # Commit any out-of-band damage to .txt + $this->_saveDamage(); + $this->ci( 0, $text, $comment, $user, $date ); +} + +=begin TML + +---++ ObjectMethod addRevisionFromStream($fh, $comment, $cUID, $date) + +Add new revision. Replace file with contents of stream. + * =$fh= filehandle for contents of new revision + * =$cUID= is a cUID. + * =$date= in epoch seconds; may be ignored + +=cut + +sub addRevisionFromStream { + my ( $this, $stream, $comment, $user, $date ) = @_; + $this->init(); + + # Commit any out-of-band damage to .txt + $this->_saveDamage(); + + $this->ci( 1, $stream, $comment, $user, $date ); +} + +=begin TML + +---++ ObjectMethod replaceRevision($text, $comment, $cUID, $date) + +Replace the top revision. + * =$text= is the new revision + * =$date= is in epoch seconds. + * =$cUID= is a cUID. + * =$comment= is a string + +=cut + +sub replaceRevision { + my $this = shift; + $this->_saveDamage(); + $this->repRev(@_); +} + +# Signature as for replaceRevision +sub repRev { + die "Pure virtual method"; +} + =begin TML ---++ ObjectMethod getRevisionHistory() -> $iterator @@ -256,7 +378,8 @@ Get an iterator over the identifiers of revisions. Returns the most recent revision first. The default is to return an iterator from the current version number -down to 1. Return rev 0 if the file exists without history. +down to 1. Return rev 1 if the file exists without history. Return +an empty iterator if the file does not exist. =cut @@ -266,7 +389,7 @@ sub getRevisionHistory { unless ( -e $this->{rcsFile} ) { require Foswiki::ListIterator; if ( -e $this->{file} ) { - return Foswiki::ListIterator->new( [0] ); + return Foswiki::ListIterator->new( [1] ); } else { return Foswiki::ListIterator->new( [] ); @@ -288,7 +411,13 @@ been no revisions committed to the store. =cut sub getLatestRevisionID { - return shift->numRevisions() || 1; + my $this = shift; + return 0 unless -e $this->{file}; + my $rev = $this->_numRevisions() || 1; + # If there is a pending pseudo-revision, need n+1, but only if there is + # an existing history + $rev++ unless $this->noCheckinPending() || !-e $this->{rcsFile}; + return $rev; } =begin TML @@ -307,7 +436,7 @@ doesn't get merged into rev 1. sub getNextRevisionID { my $this = shift; - return ( $this->numRevisions() || ( ( -e $this->{file} ) ? 1 : 0 ) ) + 1; + return $this->getLatestRevisionID() + 1; } =begin TML @@ -362,7 +491,7 @@ sub revisionExists { my ( $this, $rev ) = @_; # Rev numbers run from 1 to numRevisions - return $rev && $rev <= $this->numRevisions(); + return $rev && $rev <= $this->_numRevisions(); } =begin TML @@ -457,29 +586,6 @@ sub storedDataExists { =begin TML ----++ ObjectMethod getTimestamp() -> $integer - -Get the timestamp of the file -Returns 0 if no file, otherwise epoch seconds - -=cut - -sub getTimestamp { - my ($this) = @_; - ASSERT( $this->{file} ) if DEBUG; - - my $date = 0; - if ( -e $this->{file} ) { - - # If the stat fails, stamp it with some arbitrary static - # time in the past (00:40:05 on 5th Jan 1989) - $date = ( stat $this->{file} )[9] || 600000000; - } - return $date; -} - -=begin TML - ---++ ObjectMethod restoreLatestRevision( $cUID ) Restore the plaintext file from the revision at the head. @@ -1298,6 +1404,25 @@ sub eachChange { } } +# ObjectMethod getTimestamp() -> $integer +# Get the timestamp of the file +# Returns 0 if no file, otherwise epoch seconds +# Used in subclasses + +sub getTimestamp { + my ($this) = @_; + ASSERT( $this->{file} ) if DEBUG; + + my $date = 0; + if ( -e $this->{file} ) { + + # If the stat fails, stamp it with some arbitrary static + # time in the past (00:40:05 on 5th Jan 1989) + $date = ( stat $this->{file} )[9] || 600000000; + } + return $date; +} + 1; __END__ @@ -1358,45 +1483,6 @@ Initialise a text file. =begin TML ----++ ObjectMethod addRevisionFromText($text, $comment, $cUID, $date) - -Add new revision. Replace file with text. - * =$text= of new revision - * =$comment= checkin comment - * =$cUID= is a cUID. - * =$date= in epoch seconds; may be ignored - -*Virtual method* - must be implemented by subclasses - -=begin TML - ----++ ObjectMethod addRevisionFromStream($fh, $comment, $cUID, $date) - -Add new revision. Replace file with contents of stream. - * =$fh= filehandle for contents of new revision - * =$cUID= is a cUID. - * =$date= in epoch seconds; may be ignored - -*Virtual method* - must be implemented by subclasses - -=cut - -=begin TML - ----++ ObjectMethod replaceRevision($text, $comment, $cUID, $date) - -Replace the top revision. - * =$text= is the new revision - * =$date= is in epoch seconds. - * =$cUID= is a cUID. - * =$comment= is a string - -*Virtual method* - must be implemented by subclasses - -=cut - -=begin TML - ---++ ObjectMethod deleteRevision() Delete the last revision - do nothing if there is only one revision @@ -1436,7 +1522,7 @@ given epoch-secs time, or undef it none could be found. *Virtual method* - must be implemented by subclasses =cut -__END__ + Foswiki - The Free and Open Source Wiki, http://foswiki.org/ Copyright (C) 2008-2011 Foswiki Contributors. Foswiki Contributors diff --git a/core/lib/Foswiki/Store/VC/RcsLiteHandler.pm b/core/lib/Foswiki/Store/VC/RcsLiteHandler.pm index 4ff39f0dcc..b6dd994bb1 100644 --- a/core/lib/Foswiki/Store/VC/RcsLiteHandler.pm +++ b/core/lib/Foswiki/Store/VC/RcsLiteHandler.pm @@ -432,7 +432,7 @@ sub initText { } # implements VC::Handler -sub numRevisions { +sub _numRevisions { my ($this) = @_; _ensureProcessed($this); @@ -444,32 +444,10 @@ sub numRevisions { return $this->{head}; } -# implements VC::Handler -sub addRevisionFromText { - _addRevision( shift, 0, @_ ); -} - -# implements VC::Handler -sub addRevisionFromStream { - _addRevision( shift, 1, @_ ); -} - -sub _addRevision { +sub ci { my ( $this, $isStream, $data, $log, $author, $date ) = @_; _ensureProcessed($this); - if ( $this->{state} eq 'nocommav' && -e $this->{file} ) { - - # Must do this *before* saving the attachment, so we - # save the file on disc - $this->{head} = 1; - $this->{revs}[1]->{text} = - Foswiki::Store::VC::Handler::readFile( $this, $this->{file} ); - $this->{revs}[1]->{log} = $log; - $this->{revs}[1]->{author} = $author; - $this->{revs}[1]->{date} = ( defined $date ? $date : time() ); - _writeMe($this); - } if ($isStream) { $this->saveStream($data); @@ -480,8 +458,7 @@ sub _addRevision { else { $this->saveFile( $this->{file}, $data ); } - - my $head = $this->{head}; + my $head = $this->{head} || 0; if ($head) { my $lNew = _split($data); my $lOld = _split( $this->{revs}[$head]->{text} ); @@ -516,11 +493,11 @@ sub _writeMe { } # implements VC::Handler -sub replaceRevision { +sub repRev { my ( $this, $text, $comment, $user, $date ) = @_; _ensureProcessed($this); _delLastRevision($this); - return _addRevision( $this, 0, $text, $comment, $user, $date ); + return $this->ci( 0, $text, $comment, $user, $date ); } # implements VC::Handler @@ -572,9 +549,8 @@ sub getInfo { my ( $this, $version ) = @_; _ensureProcessed($this); - my $info; - if ( $this->{state} ne 'nocommav' ) { + if ( $this->{state} ne 'nocommav') { if ( !$version || $version > $this->{head} ) { $version = $this->{head} || 1; } @@ -584,6 +560,14 @@ sub getInfo { author => $this->{revs}[$version]->{author}, comment => $this->{revs}[$version]->{log} }; + # We have to check that there is not a pending version in the .txt + unless ($this->noCheckinPending()) { + # There's a pending version in the .txt + $info->{version}++; + $info->{author} = $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID; + $info->{comment} = "pending"; + $info->{date} = time(); + } } else { $info = $this->SUPER::getInfo($version); @@ -772,15 +756,21 @@ sub _addChunk { sub getRevisionAtTime { my ( $this, $date ) = @_; - my $version = 1; - _ensureProcessed($this); - $version = $this->{head}; + if ($this->{state} eq 'nocommav') { + return ($date >= (stat($this->{file}))[9]) ? 1 : undef; + } + + my $version = $this->{head}; while ( $this->{revs}[$version]->{date} > $date ) { $version--; - return 0 if $version == 0; + return undef if $version == 0; } + if ($version == $this->{head} && !$this->noCheckinPending()) { + # Check the file date + $version++ if ($date >= (stat($this->{file}))[9]); + } return $version; } diff --git a/core/lib/Foswiki/Store/VC/RcsWrapHandler.pm b/core/lib/Foswiki/Store/VC/RcsWrapHandler.pm index c700c7e19a..caaa1b55f7 100644 --- a/core/lib/Foswiki/Store/VC/RcsWrapHandler.pm +++ b/core/lib/Foswiki/Store/VC/RcsWrapHandler.pm @@ -101,37 +101,29 @@ sub initText { } # implements VC::Handler -sub addRevisionFromText { - my ( $this, $text, $comment, $user, $date ) = @_; - $this->init(); - - #print STDERR "Wrap: Forced save at $date $this->{file}\n" if $date; - - unless ( -e $this->{rcsFile} ) { # - # SMELL: what is this for? - _lock($this); - _ci( $this, $comment, $user, $date ); - } - Foswiki::Store::VC::Handler::saveFile( $this, $this->{file}, $text ); - _lock($this); - _ci( $this, $comment, $user, $date ); -} - -# implements VC::Handler -sub addRevisionFromStream { - my ( $this, $stream, $comment, $user, $date ) = @_; - $this->init(); +# Designed for calling *only* from the Handler superclass and this class +sub ci { + my ($this, $isStream, $data, $comment, $user, $date) = @_; +# unless ( -e $this->{rcsFile} ) { # +# # SMELL: what is this for? +# _lock($this); +# _ci( $this, $comment, $user, $date ); +# } _lock($this); - Foswiki::Store::VC::Handler::saveStream( $this, $stream ); + if ($isStream) { + $this->saveStream( $data ); + } else { + $this->saveFile( $this->{file}, $data ); + } _ci( $this, $comment, $user, $date ); } # implements VC::Handler -sub replaceRevision { +sub repRev { my ( $this, $text, $comment, $user, $date ) = @_; - my $rev = $this->numRevisions() || 0; + my $rev = $this->_numRevisions() || 0; $comment ||= 'none'; @@ -167,7 +159,7 @@ sub replaceRevision { # implements VC::Handler sub deleteRevision { my ($this) = @_; - my $rev = $this->numRevisions(); + my $rev = $this->_numRevisions(); return if ( $rev <= 1 ); return _deleteRevision( $this, $rev ); } @@ -279,43 +271,13 @@ sub getRevision { return ($text, $isLatest); } -# implements VC::Handler -sub numRevisions { - my $this = shift; - - unless ( -e $this->{rcsFile} ) { - - # If there is no history, there can only be one. - return 1 if -e $this->{file}; - return 0; - } - - my ( $rcsOutput, $exit ) = - Foswiki::Sandbox->sysCommand( $Foswiki::cfg{RCS}{histCmd}, - FILENAME => $this->{rcsFile} ); - if ($exit) { - throw Error::Simple( 'RCS: ' - . $Foswiki::cfg{RCS}{histCmd} . ' of ' - . $this->hidePath( $this->{rcsFile} ) - . ' failed: ' - . $rcsOutput ); - } - if ( $rcsOutput =~ /head:\s+\d+\.(\d+)\n/ ) { - return $1; - } - if ( $rcsOutput =~ /total revisions: (\d+)\n/ ) { - return $1; - } - return 1; -} - # implements VC::Handler sub getInfo { my ( $this, $version ) = @_; - if ( -e $this->{rcsFile} ) { - if ( !$version || $version > $this->numRevisions() ) { - $version = $this->numRevisions(); + if ( $this->noCheckinPending() ) { + if ( !$version || $version > $this->_numRevisions() ) { + $version = $this->_numRevisions(); } my ( $rcsOut, $exit ) = Foswiki::Sandbox->sysCommand( $Foswiki::cfg{RCS}{infoCmd}, @@ -343,6 +305,36 @@ sub getInfo { return $this->SUPER::getInfo($version); } +# implements VC::Handler +sub _numRevisions { + my $this = shift; + + unless ( -e $this->{rcsFile} ) { + + # If there is no history, there can only be one. + return 1 if -e $this->{file}; + return 0; + } + + my ( $rcsOutput, $exit ) = + Foswiki::Sandbox->sysCommand( $Foswiki::cfg{RCS}{histCmd}, + FILENAME => $this->{rcsFile} ); + if ($exit) { + throw Error::Simple( 'RCS: ' + . $Foswiki::cfg{RCS}{histCmd} . ' of ' + . $this->hidePath( $this->{rcsFile} ) + . ' failed: ' + . $rcsOutput ); + } + if ( $rcsOutput =~ /head:\s+\d+\.(\d+)\n/ ) { + return $1; + } + if ( $rcsOutput =~ /total revisions: (\d+)\n/ ) { + return $1; + } + return 1; +} + # implements VC::Handler # rev1 is the lower, rev2 is the higher revision sub revisionDiff { @@ -525,21 +517,28 @@ sub _lock { sub getRevisionAtTime { my ( $this, $date ) = @_; - if ( !-e $this->{rcsFile} ) { - return; + unless( -e $this->{rcsFile} ) { + return ($date >= (stat($this->{file}))[9]) ? 1 : undef; } + require Foswiki::Time; - $date = Foswiki::Time::formatTime( $date, '$rcs', 'gmtime' ); + my $sdate = Foswiki::Time::formatTime( $date, '$rcs', 'gmtime' ); my ( $rcsOutput, $exit ) = Foswiki::Sandbox->sysCommand( $Foswiki::cfg{RCS}{rlogDateCmd}, - DATE => $date, + DATE => $sdate, FILENAME => $this->{file} ); + my $version = undef; if ( $rcsOutput =~ m/revision \d+\.(\d+)/ ) { - return $1; + $version = $1; } - return 1; + + if ($version && !$this->noCheckinPending()) { + # Check the file date + $version++ if ($date >= (stat($this->{file}))[9]); + } + return $version; } 1; diff --git a/core/lib/Foswiki/Store/VC/Store.pm b/core/lib/Foswiki/Store/VC/Store.pm index 51b12813d2..1f71dc12b2 100644 --- a/core/lib/Foswiki/Store/VC/Store.pm +++ b/core/lib/Foswiki/Store/VC/Store.pm @@ -89,19 +89,23 @@ sub readTopic { $text =~ s/\r//g; # Remove carriage returns $topicObject->setEmbeddedStoreForm($text); + unless ($handler->noCheckinPending()) { + # If a checkin is pending, fix the TOPICINFO + my $ri = $topicObject->get('TOPICINFO'); + my $truth = $handler->getInfo($version); + for my $i qw(author version date) { + $ri->{$i} = $truth->{$i}; + } + } + my $gotRev = $version; unless ( defined $gotRev ) { - # First try the just-loaded text for the revision + # First try the just-loaded for the revision my $ri = $topicObject->get('TOPICINFO'); - if ( defined($ri) ) { - - # SMELL: this can end up overriding a correct rev no (the one - # requested) with an incorrect one (the one in the TOPICINFO) - $gotRev = $ri->{version}; - } + $gotRev = $ri->{version} if defined $ri; } - if ( !$gotRev ) { + if ( !defined $gotRev ) { # No revision from any other source; must be latest $gotRev = $handler->getLatestRevisionID(); @@ -272,12 +276,15 @@ sub saveTopic { my $handler = $this->getHandler($topicObject); + # just in case they are not sequential + my $nextRev = $handler->getNextRevisionID(); + my $ti = $topicObject->get('TOPICINFO'); + $ti->{version} = $nextRev; + $ti->{author} = $cUID; + $handler->addRevisionFromText( $topicObject->getEmbeddedStoreForm(), 'save topic', $cUID, $options->{forcedate} ); - # just in case they are not sequential - my $nextRev = $handler->getLatestRevisionID(); - my $extra = $options->{minor} ? 'minor' : ''; $handler->recordChange( $cUID, $nextRev, $extra ); @@ -288,11 +295,10 @@ sub repRev { my ( $this, $topicObject, $cUID, %options ) = @_; ASSERT( $topicObject->isa('Foswiki::Meta') ) if DEBUG; ASSERT($cUID) if DEBUG; - my $info = $topicObject->getRevisionInfo(); my $handler = $this->getHandler($topicObject); $handler->replaceRevision( $topicObject->getEmbeddedStoreForm(), - 'reprev', $info->{author}, $info->{date} ); + 'reprev', $cUID, $info->{date} ); my $rev = $handler->getLatestRevisionID(); $handler->recordChange( $cUID, $rev, 'minor, reprev' ); return $rev;