@@ -244,6 +244,52 @@ func TestMergeVersionedFiles(t *testing.T) {
244244 }
245245}
246246
247+ func TestMergeVersionedFiles_SemanticOrder (t * testing.T ) {
248+ tmpDir , changesDir := setupTestChangesDir (t )
249+
250+ createVersionFiles (t , changesDir , map [string ]string {
251+ "v0.1.0.md" : "## v0.1.0\n \n First version\n " ,
252+ "v0.1.2.md" : "## v0.1.2\n \n Patch release\n " ,
253+ "v0.2.0.md" : "## v0.2.0\n \n Second minor\n " ,
254+ "v0.9.1.md" : "## v0.9.1\n \n Ninth minor patch\n " ,
255+ "v0.10.0.md" : "## v0.10.0\n \n Tenth minor\n " ,
256+ })
257+
258+ changelogPath := filepath .Join (tmpDir , "CHANGELOG.md" )
259+ g := createTestGenerator (t , changesDir , changelogPath )
260+
261+ if err := g .MergeVersionedFiles (); err != nil {
262+ t .Fatalf ("merge failed: %v" , err )
263+ }
264+
265+ content := string (readChangelogContent (t , changelogPath ))
266+
267+ // v0.10.0 must appear before v0.9.1 (not between v0.1.2 and v0.2.0)
268+ pos010 := strings .Index (content , "## v0.10.0" )
269+ pos091 := strings .Index (content , "## v0.9.1" )
270+ pos020 := strings .Index (content , "## v0.2.0" )
271+ pos012 := strings .Index (content , "## v0.1.2" )
272+ pos010v := strings .Index (content , "## v0.1.0" )
273+
274+ if pos010 == - 1 || pos091 == - 1 || pos020 == - 1 || pos012 == - 1 || pos010v == - 1 {
275+ t .Fatalf ("not all versions found in merged content:\n %s" , content )
276+ }
277+
278+ // Correct descending order: v0.10.0 > v0.9.1 > v0.2.0 > v0.1.2 > v0.1.0
279+ if pos010 > pos091 {
280+ t .Errorf ("v0.10.0 (pos %d) should appear before v0.9.1 (pos %d)" , pos010 , pos091 )
281+ }
282+ if pos091 > pos020 {
283+ t .Errorf ("v0.9.1 (pos %d) should appear before v0.2.0 (pos %d)" , pos091 , pos020 )
284+ }
285+ if pos020 > pos012 {
286+ t .Errorf ("v0.2.0 (pos %d) should appear before v0.1.2 (pos %d)" , pos020 , pos012 )
287+ }
288+ if pos012 > pos010v {
289+ t .Errorf ("v0.1.2 (pos %d) should appear before v0.1.0 (pos %d)" , pos012 , pos010v )
290+ }
291+ }
292+
247293func TestMergeVersionedFiles_EmptyDir (t * testing.T ) {
248294 tmpDir := t .TempDir ()
249295 changesDir := filepath .Join (tmpDir , ".changes" )
@@ -546,22 +592,145 @@ Some description about this project.
546592}
547593
548594func TestSortVersionFiles (t * testing.T ) {
595+ tests := []struct {
596+ name string
597+ input []string
598+ expected []string
599+ }{
600+ {
601+ name : "basic ordering" ,
602+ input : []string {
603+ "/tmp/.changes/v0.1.0.md" ,
604+ "/tmp/.changes/v1.0.0.md" ,
605+ "/tmp/.changes/v0.9.0.md" ,
606+ },
607+ expected : []string {
608+ "/tmp/.changes/v1.0.0.md" ,
609+ "/tmp/.changes/v0.9.0.md" ,
610+ "/tmp/.changes/v0.1.0.md" ,
611+ },
612+ },
613+ {
614+ name : "double digit minor version sorted semantically not lexicographically" ,
615+ input : []string {
616+ "/tmp/.changes/v0.1.0.md" ,
617+ "/tmp/.changes/v0.1.2.md" ,
618+ "/tmp/.changes/v0.2.0.md" ,
619+ "/tmp/.changes/v0.10.0.md" ,
620+ "/tmp/.changes/v0.9.1.md" ,
621+ },
622+ expected : []string {
623+ "/tmp/.changes/v0.10.0.md" ,
624+ "/tmp/.changes/v0.9.1.md" ,
625+ "/tmp/.changes/v0.2.0.md" ,
626+ "/tmp/.changes/v0.1.2.md" ,
627+ "/tmp/.changes/v0.1.0.md" ,
628+ },
629+ },
630+ {
631+ name : "mixed major and minor versions" ,
632+ input : []string {
633+ "/tmp/.changes/v0.1.0.md" ,
634+ "/tmp/.changes/v2.0.0.md" ,
635+ "/tmp/.changes/v0.10.0.md" ,
636+ "/tmp/.changes/v1.0.0.md" ,
637+ "/tmp/.changes/v1.10.0.md" ,
638+ "/tmp/.changes/v1.9.0.md" ,
639+ },
640+ expected : []string {
641+ "/tmp/.changes/v2.0.0.md" ,
642+ "/tmp/.changes/v1.10.0.md" ,
643+ "/tmp/.changes/v1.9.0.md" ,
644+ "/tmp/.changes/v1.0.0.md" ,
645+ "/tmp/.changes/v0.10.0.md" ,
646+ "/tmp/.changes/v0.1.0.md" ,
647+ },
648+ },
649+ {
650+ name : "single element" ,
651+ input : []string {
652+ "/tmp/.changes/v1.0.0.md" ,
653+ },
654+ expected : []string {
655+ "/tmp/.changes/v1.0.0.md" ,
656+ },
657+ },
658+ {
659+ name : "empty slice" ,
660+ input : []string {},
661+ expected : []string {},
662+ },
663+ }
664+
665+ for _ , tt := range tests {
666+ t .Run (tt .name , func (t * testing.T ) {
667+ // Copy to avoid mutating test data
668+ files := make ([]string , len (tt .input ))
669+ copy (files , tt .input )
670+
671+ sortVersionFiles (files )
672+
673+ if len (files ) != len (tt .expected ) {
674+ t .Fatalf ("expected %d files, got %d" , len (tt .expected ), len (files ))
675+ }
676+ for i , want := range tt .expected {
677+ if files [i ] != want {
678+ t .Errorf ("position %d: expected %s, got %s" , i , want , files [i ])
679+ }
680+ }
681+ })
682+ }
683+ }
684+
685+ func TestSortVersionFiles_SemanticOrder_0_10_0 (t * testing.T ) {
686+ // Regression test: v0.10.0 must sort AFTER v0.9.1 and not between v0.1.2 and v0.2.0.
687+ // This was the original bug: lexicographic comparison treated "10" < "2" because
688+ // "1" < "2" character-by-character.
549689 files := []string {
550- "/tmp/.changes/v0.1.0.md" ,
551- "/tmp/.changes/v1.0.0.md" ,
552- "/tmp/.changes/v0.9.0.md" ,
690+ "/tmp/.changes/v0.1.2.md" ,
691+ "/tmp/.changes/v0.2.0.md" ,
692+ "/tmp/.changes/v0.10.0.md" ,
693+ "/tmp/.changes/v0.9.1.md" ,
553694 }
554695
555696 sortVersionFiles (files )
556697
557- // Should be in reverse order (newest first)
558- if files [0 ] != "/tmp/.changes/v1.0.0.md" {
559- t .Errorf ("expected v1.0.0.md first, got %s" , files [0 ])
698+ // Newest first (descending semantic order)
699+ expected := []string {
700+ "/tmp/.changes/v0.10.0.md" ,
701+ "/tmp/.changes/v0.9.1.md" ,
702+ "/tmp/.changes/v0.2.0.md" ,
703+ "/tmp/.changes/v0.1.2.md" ,
560704 }
561- if files [1 ] != "/tmp/.changes/v0.9.0.md" {
562- t .Errorf ("expected v0.9.0.md second, got %s" , files [1 ])
705+
706+ for i , want := range expected {
707+ if files [i ] != want {
708+ t .Errorf ("position %d: expected %s, got %s" , i , want , files [i ])
709+ }
563710 }
564- if files [2 ] != "/tmp/.changes/v0.1.0.md" {
565- t .Errorf ("expected v0.1.0.md third, got %s" , files [2 ])
711+ }
712+
713+ func TestExtractVersion (t * testing.T ) {
714+ tests := []struct {
715+ path string
716+ wantStr string
717+ }{
718+ {"/tmp/.changes/v1.2.3.md" , "1.2.3" },
719+ {"/tmp/.changes/v0.10.0.md" , "0.10.0" },
720+ {"v0.1.0.md" , "0.1.0" },
721+ {"/some/path/v2.0.0-rc.1.md" , "2.0.0-rc.1" },
722+ // Unparseable returns zero value
723+ {"/tmp/.changes/README.md" , "0.0.0" },
724+ {"/tmp/.changes/not-version.md" , "0.0.0" },
725+ }
726+
727+ for _ , tt := range tests {
728+ t .Run (tt .path , func (t * testing.T ) {
729+ v := extractVersion (tt .path )
730+ got := v .String ()
731+ if got != tt .wantStr {
732+ t .Errorf ("extractVersion(%q) = %q, want %q" , tt .path , got , tt .wantStr )
733+ }
734+ })
566735 }
567736}
0 commit comments