OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / unit / diff-parser.test.js

294 lines · javascript ref:
/**
 * Unit tests for lib/diff-parser.js
 */

import test from 'brittle'
import { parseDiff, parseCommitInfo, getDiffStats, getStatusLabel, toSideBySide } from '../../lib/diff-parser.js'
import { sampleDiffs } from '../helpers/fixtures.js'

// ========================================
// parseDiff tests
// ========================================

test('parseDiff - returns empty array for empty input', async (t) => {
  t.alike(parseDiff(''), [])
  t.alike(parseDiff(null), [])
  t.alike(parseDiff(undefined), [])
})

test('parseDiff - parses single file addition', async (t) => {
  const result = parseDiff(sampleDiffs.singleFileAdd)

  t.is(result.length, 1)
  t.is(result[0].status, 'added')
  t.is(result[0].newPath, 'newfile.js')
  t.is(result[0].additions, 3)
  t.is(result[0].deletions, 0)
  t.is(result[0].isBinary, false)
  t.is(result[0].hunks.length, 1)
})

test('parseDiff - parses single file modification', async (t) => {
  const result = parseDiff(sampleDiffs.singleFileModify)

  t.is(result.length, 1)
  t.is(result[0].status, 'modified')
  t.is(result[0].oldPath, 'file.js')
  t.is(result[0].newPath, 'file.js')
  t.ok(result[0].additions >= 1)
  t.ok(result[0].deletions >= 1)
})

test('parseDiff - parses file deletion', async (t) => {
  const result = parseDiff(sampleDiffs.fileDelete)

  t.is(result.length, 1)
  t.is(result[0].status, 'deleted')
  t.is(result[0].oldPath, 'oldfile.js')
  t.is(result[0].deletions, 2)
  t.is(result[0].additions, 0)
})

test('parseDiff - parses file rename', async (t) => {
  const result = parseDiff(sampleDiffs.fileRename)

  t.is(result.length, 1)
  t.is(result[0].status, 'renamed')
  t.is(result[0].oldPath, 'old-name.js')
  t.is(result[0].newPath, 'new-name.js')
})

test('parseDiff - parses multiple files', async (t) => {
  const result = parseDiff(sampleDiffs.multipleFiles)

  t.is(result.length, 2)
  t.is(result[0].newPath, 'file1.js')
  t.is(result[1].newPath, 'file2.js')
})

test('parseDiff - handles binary files', async (t) => {
  const result = parseDiff(sampleDiffs.binaryFile)

  t.is(result.length, 1)
  t.is(result[0].isBinary, true)
  t.is(result[0].newPath, 'image.png')
})

test('parseDiff - parses hunk headers correctly', async (t) => {
  const diff = `diff --git a/test.js b/test.js
index abc..def 100644
--- a/test.js
+++ b/test.js
@@ -10,5 +10,6 @@ function test() {
 context line
-deleted line
+added line
 context line`

  const result = parseDiff(diff)

  t.is(result.length, 1)
  t.is(result[0].hunks.length, 1)
  t.is(result[0].hunks[0].oldStart, 10)
  t.is(result[0].hunks[0].newStart, 10)
})

test('parseDiff - tracks line numbers correctly', async (t) => {
  const diff = `diff --git a/test.js b/test.js
index abc..def 100644
--- a/test.js
+++ b/test.js
@@ -1,3 +1,4 @@
 line1
+new line
 line2
 line3`

  const result = parseDiff(diff)
  const lines = result[0].hunks[0].lines

  // First line is context at line 1
  t.is(lines[0].type, 'ctx')
  t.is(lines[0].oldLineNo, 1)
  t.is(lines[0].newLineNo, 1)

  // Second line is addition at new line 2
  t.is(lines[1].type, 'add')
  t.is(lines[1].oldLineNo, null)
  t.is(lines[1].newLineNo, 2)

  // Third line is context at old line 2, new line 3
  t.is(lines[2].type, 'ctx')
  t.is(lines[2].oldLineNo, 2)
  t.is(lines[2].newLineNo, 3)
})

// ========================================
// parseCommitInfo tests
// ========================================

test('parseCommitInfo - returns null for empty input', async (t) => {
  t.is(parseCommitInfo(''), null)
  t.is(parseCommitInfo(null), null)
})

test('parseCommitInfo - parses commit info correctly', async (t) => {
  const raw = `abc123def456
parent1 parent2
John Doe
john@example.com
1700000000
Fix the bug

This is the commit body.
Multiple lines are supported.`

  const result = parseCommitInfo(raw)

  t.is(result.hash, 'abc123def456')
  t.is(result.shortHash, 'abc123d')
  t.alike(result.parents, ['parent1', 'parent2'])
  t.is(result.author, 'John Doe')
  t.is(result.email, 'john@example.com')
  t.is(result.subject, 'Fix the bug')
  t.ok(result.body.includes('This is the commit body'))
})

test('parseCommitInfo - handles single parent', async (t) => {
  const raw = `abc123
def456
Author
author@test.com
1700000000
Subject`

  const result = parseCommitInfo(raw)
  t.alike(result.parents, ['def456'])
})

test('parseCommitInfo - handles root commit (no parents)', async (t) => {
  const raw = `abc123

Author
author@test.com
1700000000
Initial commit`

  const result = parseCommitInfo(raw)
  t.alike(result.parents, [])
})

// ========================================
// getDiffStats tests
// ========================================

test('getDiffStats - calculates stats correctly', async (t) => {
  const files = [
    { additions: 10, deletions: 5 },
    { additions: 20, deletions: 3 },
    { additions: 0, deletions: 15 }
  ]

  const stats = getDiffStats(files)

  t.is(stats.filesChanged, 3)
  t.is(stats.additions, 30)
  t.is(stats.deletions, 23)
  t.is(stats.total, 53)
})

test('getDiffStats - handles empty array', async (t) => {
  const stats = getDiffStats([])

  t.is(stats.filesChanged, 0)
  t.is(stats.additions, 0)
  t.is(stats.deletions, 0)
  t.is(stats.total, 0)
})

// ========================================
// getStatusLabel tests
// ========================================

test('getStatusLabel - returns correct labels', async (t) => {
  t.is(getStatusLabel('added').label, 'Added')
  t.is(getStatusLabel('deleted').label, 'Deleted')
  t.is(getStatusLabel('modified').label, 'Modified')
  t.is(getStatusLabel('renamed').label, 'Renamed')
})

test('getStatusLabel - returns modified for unknown status', async (t) => {
  t.is(getStatusLabel('unknown').label, 'Modified')
  t.is(getStatusLabel('').label, 'Modified')
})

// ========================================
// toSideBySide tests
// ========================================

test('toSideBySide - handles context lines', async (t) => {
  const lines = [
    { type: 'ctx', content: 'line1', oldLineNo: 1, newLineNo: 1 },
    { type: 'ctx', content: 'line2', oldLineNo: 2, newLineNo: 2 }
  ]

  const result = toSideBySide(lines)

  t.is(result.length, 2)
  t.is(result[0].left.content, 'line1')
  t.is(result[0].right.content, 'line1')
})

test('toSideBySide - pairs deletions with additions', async (t) => {
  const lines = [
    { type: 'del', content: 'old', oldLineNo: 1, newLineNo: null },
    { type: 'add', content: 'new', oldLineNo: null, newLineNo: 1 }
  ]

  const result = toSideBySide(lines)

  t.is(result.length, 1)
  t.is(result[0].left.content, 'old')
  t.is(result[0].right.content, 'new')
})

test('toSideBySide - handles unmatched additions', async (t) => {
  const lines = [
    { type: 'add', content: 'new1', oldLineNo: null, newLineNo: 1 },
    { type: 'add', content: 'new2', oldLineNo: null, newLineNo: 2 }
  ]

  const result = toSideBySide(lines)

  t.is(result.length, 2)
  t.is(result[0].left, null)
  t.is(result[0].right.content, 'new1')
  t.is(result[1].left, null)
  t.is(result[1].right.content, 'new2')
})

test('toSideBySide - handles mixed changes', async (t) => {
  const lines = [
    { type: 'ctx', content: 'context', oldLineNo: 1, newLineNo: 1 },
    { type: 'del', content: 'deleted1', oldLineNo: 2, newLineNo: null },
    { type: 'del', content: 'deleted2', oldLineNo: 3, newLineNo: null },
    { type: 'add', content: 'added1', oldLineNo: null, newLineNo: 2 },
    { type: 'ctx', content: 'more context', oldLineNo: 4, newLineNo: 3 }
  ]

  const result = toSideBySide(lines)

  t.is(result.length, 4)
  // Context
  t.is(result[0].left.content, 'context')
  t.is(result[0].right.content, 'context')
  // Deletion + Addition paired
  t.is(result[1].left.content, 'deleted1')
  t.is(result[1].right.content, 'added1')
  // Unmatched deletion
  t.is(result[2].left.content, 'deleted2')
  t.is(result[2].right, null)
  // Context
  t.is(result[3].left.content, 'more context')
})