upload-progress.test.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. createUploadProgressTracker,
  4. formatEta,
  5. formatProgressMessage,
  6. formatTtyProgressLines,
  7. fitFileNameToWidth,
  8. truncateFileName,
  9. } from '../../scripts/upload-progress.mjs'
  10. describe('upload progress tracker', () => {
  11. it('aggregates total files and bytes across multiple uploads', () => {
  12. let currentTime = 0
  13. const tracker = createUploadProgressTracker({
  14. isTTY: false,
  15. log() {},
  16. now: () => currentTime,
  17. totalBytes: 400,
  18. totalFiles: 2,
  19. })
  20. tracker.startFile({ name: 'first.zip', size: 100 }, 1)
  21. currentTime = 1000
  22. tracker.advance(100)
  23. tracker.completeFile()
  24. tracker.startFile({ name: 'second.zip', size: 300 }, 2)
  25. currentTime = 2000
  26. tracker.advance(60)
  27. const snapshot = tracker.getSnapshot()
  28. expect(snapshot.currentFileIndex).toBe(2)
  29. expect(snapshot.totalFiles).toBe(2)
  30. expect(snapshot.totalBytes).toBe(400)
  31. expect(snapshot.totalUploadedBytes).toBe(160)
  32. expect(snapshot.currentFileBytes).toBe(60)
  33. expect(snapshot.totalPercent).toBeCloseTo(40)
  34. expect(snapshot.currentFilePercent).toBeCloseTo(20)
  35. expect(snapshot.speedBytesPerSecond).toBeCloseTo(80)
  36. })
  37. it('reports a safe eta before any bytes are uploaded', () => {
  38. const tracker = createUploadProgressTracker({
  39. isTTY: false,
  40. log() {},
  41. now: () => 0,
  42. totalBytes: 200,
  43. totalFiles: 1,
  44. })
  45. tracker.startFile({ name: 'asset.zip', size: 200 }, 1)
  46. expect(tracker.getSnapshot().etaSeconds).toBeNull()
  47. expect(formatEta(tracker.getSnapshot().etaSeconds)).toBe('--:--')
  48. })
  49. it('reaches 100 percent after the last chunk and finish', () => {
  50. let currentTime = 0
  51. const tracker = createUploadProgressTracker({
  52. isTTY: false,
  53. log() {},
  54. now: () => currentTime,
  55. totalBytes: 120,
  56. totalFiles: 1,
  57. })
  58. tracker.startFile({ name: 'asset.zip', size: 120 }, 1)
  59. currentTime = 1000
  60. tracker.advance(120)
  61. tracker.completeFile()
  62. tracker.finish()
  63. const snapshot = tracker.getSnapshot()
  64. expect(snapshot.totalUploadedBytes).toBe(120)
  65. expect(snapshot.totalPercent).toBe(100)
  66. expect(snapshot.currentFilePercent).toBe(100)
  67. expect(snapshot.etaSeconds).toBe(0)
  68. })
  69. it('can roll back the current file progress before a retry', () => {
  70. let currentTime = 0
  71. const tracker = createUploadProgressTracker({
  72. isTTY: false,
  73. log() {},
  74. now: () => currentTime,
  75. totalBytes: 400,
  76. totalFiles: 2,
  77. })
  78. tracker.startFile({ name: 'first.zip', size: 100 }, 1)
  79. currentTime = 1000
  80. tracker.advance(100)
  81. tracker.completeFile()
  82. tracker.startFile({ name: 'second.zip', size: 300 }, 2)
  83. currentTime = 2000
  84. tracker.advance(120)
  85. tracker.resetCurrentFile()
  86. const snapshot = tracker.getSnapshot()
  87. expect(snapshot.totalUploadedBytes).toBe(100)
  88. expect(snapshot.currentFileBytes).toBe(0)
  89. expect(snapshot.totalPercent).toBeCloseTo(25)
  90. expect(snapshot.currentFilePercent).toBe(0)
  91. })
  92. it('throttles non-tty progress logs instead of logging every chunk', () => {
  93. let currentTime = 0
  94. const logs: string[] = []
  95. const tracker = createUploadProgressTracker({
  96. isTTY: false,
  97. log(message) {
  98. logs.push(message)
  99. },
  100. now: () => currentTime,
  101. percentStep: 5,
  102. throttleMs: 1000,
  103. totalBytes: 1000,
  104. totalFiles: 1,
  105. })
  106. tracker.startFile({ name: 'asset.zip', size: 1000 }, 1)
  107. currentTime = 100
  108. tracker.advance(10)
  109. currentTime = 200
  110. tracker.advance(20)
  111. currentTime = 300
  112. tracker.advance(30)
  113. expect(logs).toHaveLength(2)
  114. expect(logs[0]).toContain('file 1/1')
  115. expect(logs[1]).toContain('progress 6.0%')
  116. })
  117. it('interrupts an active tty progress bar before printing status messages', () => {
  118. const logs: string[] = []
  119. const writes: string[] = []
  120. class FakeProgressBar {
  121. complete = false
  122. curr = 0
  123. total = 100
  124. interrupt(message: string) {
  125. writes.push(`interrupt:${message}`)
  126. }
  127. render() {}
  128. terminate() {}
  129. tick(delta: number) {
  130. this.curr += delta
  131. }
  132. update(ratio: number) {
  133. this.curr = Math.floor(this.total * ratio)
  134. }
  135. }
  136. const fakeStream = {
  137. clearLine() {},
  138. cursorTo() {},
  139. isTTY: true,
  140. moveCursor() {},
  141. write(chunk: string) {
  142. writes.push(chunk)
  143. return true
  144. },
  145. }
  146. const tracker = createUploadProgressTracker({
  147. ProgressBarClass: FakeProgressBar as never,
  148. isTTY: true,
  149. log(message) {
  150. logs.push(message)
  151. },
  152. stream: fakeStream as never,
  153. totalBytes: 100,
  154. totalFiles: 1,
  155. })
  156. tracker.startFile({ name: 'asset.zip', size: 100 }, 1)
  157. tracker.interrupt('[release:upload] replacing existing asset asset.zip')
  158. expect(logs).toEqual([])
  159. expect(writes.length).toBeGreaterThan(0)
  160. expect(writes.join('')).toContain('[release:upload] replacing existing asset asset.zip')
  161. })
  162. it('truncates long file names while keeping the suffix visible', () => {
  163. const fileName = 'SwitchHosts-macos-universal-v4.2.0.12345-very-long-artifact-name.zip'
  164. const truncated = truncateFileName(fileName, 36)
  165. expect(truncated).toHaveLength(36)
  166. expect(truncated).toContain('...')
  167. expect(truncated.endsWith('ifact-name.zip')).toBe(true)
  168. expect(
  169. formatProgressMessage({
  170. currentFileIndex: 1,
  171. currentFilePercent: 12.34,
  172. currentFileSize: 10,
  173. currentFileBytes: 1,
  174. currentFileName: fileName,
  175. displayFileName: truncated,
  176. etaLabel: '00:12',
  177. etaSeconds: 12,
  178. speedBytesPerSecond: 10,
  179. speedLabel: '10 B/s',
  180. totalBytes: 100,
  181. totalFiles: 2,
  182. totalPercent: 45.67,
  183. totalUploadedBytes: 46,
  184. totalLabel: '100 B',
  185. transferredLabel: '46 B',
  186. }),
  187. ).toContain(truncated)
  188. })
  189. it('shows the full file name in tty output when there is enough space', () => {
  190. const fileName = 'SwitchHosts-v4.3.0.6136-linux-amd64.deb'
  191. const lines = formatTtyProgressLines(
  192. {
  193. currentFileIndex: 5,
  194. currentFilePercent: 30.4,
  195. currentFileSize: 100,
  196. currentFileBytes: 30,
  197. currentFileName: fileName,
  198. displayFileName: fileName,
  199. etaLabel: '02:21:58',
  200. etaSeconds: 8518,
  201. speedBytesPerSecond: 175000,
  202. speedLabel: '175 kB/s',
  203. totalBytes: 1520000000,
  204. totalFiles: 24,
  205. totalPercent: 1.9,
  206. totalUploadedBytes: 28100000,
  207. totalLabel: '1.52 GB',
  208. transferredLabel: '28.1 MB',
  209. },
  210. '[ ]',
  211. 140,
  212. )
  213. expect(lines[1]).toContain(fileName)
  214. })
  215. it('truncates the tty file name only when the line is too narrow', () => {
  216. const fileName = 'SwitchHosts-v4.3.0.6136-linux-amd64.deb'
  217. expect(fitFileNameToWidth(fileName, 100)).toBe(fileName)
  218. expect(fitFileNameToWidth(fileName, 20)).toContain('...')
  219. })
  220. })