statbtrfs 0.30__tar.gz → 0.32__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {statbtrfs-0.30 → statbtrfs-0.32}/PKG-INFO +7 -1
- {statbtrfs-0.30 → statbtrfs-0.32}/README.txt +7 -1
- {statbtrfs-0.30 → statbtrfs-0.32}/statbtrfs.egg-info/PKG-INFO +7 -1
- statbtrfs-0.32/statbtrfs.egg-info/SOURCES.txt +19 -0
- {statbtrfs-0.30 → statbtrfs-0.32}/statbtrfs.py +106 -89
- statbtrfs-0.32/tests/test_cli.py +112 -0
- statbtrfs-0.32/tests/test_color.py +10 -0
- statbtrfs-0.32/tests/test_filter_recent.py +53 -0
- statbtrfs-0.32/tests/test_import_seams.py +36 -0
- statbtrfs-0.32/tests/test_live.py +39 -0
- statbtrfs-0.32/tests/test_mounts.py +27 -0
- statbtrfs-0.32/tests/test_parse_scrub.py +56 -0
- statbtrfs-0.32/tests/test_parse_show.py +25 -0
- statbtrfs-0.32/tests/test_parse_stats.py +42 -0
- statbtrfs-0.32/tests/test_scrub_orchestration.py +203 -0
- statbtrfs-0.30/statbtrfs.egg-info/SOURCES.txt +0 -9
- {statbtrfs-0.30 → statbtrfs-0.32}/setup.cfg +0 -0
- {statbtrfs-0.30 → statbtrfs-0.32}/setup.py +0 -0
- {statbtrfs-0.30 → statbtrfs-0.32}/statbtrfs.egg-info/dependency_links.txt +0 -0
- {statbtrfs-0.30 → statbtrfs-0.32}/statbtrfs.egg-info/entry_points.txt +0 -0
- {statbtrfs-0.30 → statbtrfs-0.32}/statbtrfs.egg-info/requires.txt +0 -0
- {statbtrfs-0.30 → statbtrfs-0.32}/statbtrfs.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: statbtrfs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32
|
|
4
4
|
Summary: Query Btrfs filesystem status
|
|
5
5
|
Home-page: https://github.com/yufei-pan/statbtrfs
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -45,3 +45,9 @@ optional arguments:
|
|
|
45
45
|
|
|
46
46
|
Install:
|
|
47
47
|
sudo pip install statbtrfs
|
|
48
|
+
|
|
49
|
+
Development:
|
|
50
|
+
pip install -r requirements-dev.txt
|
|
51
|
+
pytest # unit tests (offline)
|
|
52
|
+
pytest --cov=statbtrfs --cov-fail-under=85
|
|
53
|
+
pytest -m live # optional status-only smoke on a btrfs host
|
|
@@ -15,4 +15,10 @@ optional arguments:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
Install:
|
|
18
|
-
sudo pip install statbtrfs
|
|
18
|
+
sudo pip install statbtrfs
|
|
19
|
+
|
|
20
|
+
Development:
|
|
21
|
+
pip install -r requirements-dev.txt
|
|
22
|
+
pytest # unit tests (offline)
|
|
23
|
+
pytest --cov=statbtrfs --cov-fail-under=85
|
|
24
|
+
pytest -m live # optional status-only smoke on a btrfs host
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: statbtrfs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32
|
|
4
4
|
Summary: Query Btrfs filesystem status
|
|
5
5
|
Home-page: https://github.com/yufei-pan/statbtrfs
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -45,3 +45,9 @@ optional arguments:
|
|
|
45
45
|
|
|
46
46
|
Install:
|
|
47
47
|
sudo pip install statbtrfs
|
|
48
|
+
|
|
49
|
+
Development:
|
|
50
|
+
pip install -r requirements-dev.txt
|
|
51
|
+
pytest # unit tests (offline)
|
|
52
|
+
pytest --cov=statbtrfs --cov-fail-under=85
|
|
53
|
+
pytest -m live # optional status-only smoke on a btrfs host
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.txt
|
|
2
|
+
setup.py
|
|
3
|
+
statbtrfs.py
|
|
4
|
+
statbtrfs.egg-info/PKG-INFO
|
|
5
|
+
statbtrfs.egg-info/SOURCES.txt
|
|
6
|
+
statbtrfs.egg-info/dependency_links.txt
|
|
7
|
+
statbtrfs.egg-info/entry_points.txt
|
|
8
|
+
statbtrfs.egg-info/requires.txt
|
|
9
|
+
statbtrfs.egg-info/top_level.txt
|
|
10
|
+
tests/test_cli.py
|
|
11
|
+
tests/test_color.py
|
|
12
|
+
tests/test_filter_recent.py
|
|
13
|
+
tests/test_import_seams.py
|
|
14
|
+
tests/test_live.py
|
|
15
|
+
tests/test_mounts.py
|
|
16
|
+
tests/test_parse_scrub.py
|
|
17
|
+
tests/test_parse_show.py
|
|
18
|
+
tests/test_parse_stats.py
|
|
19
|
+
tests/test_scrub_orchestration.py
|
|
@@ -8,92 +8,101 @@ import fnmatch
|
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
try:
|
|
10
10
|
import multiCMD # type: ignore
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
if tuple(int(x) for x in multiCMD.version.split('.')) < (1, 51):
|
|
12
|
+
raise ImportError('multiCMD too old')
|
|
13
|
+
except (ImportError, AttributeError, ValueError):
|
|
13
14
|
import sys,types,base64,lzma # noqa: E401
|
|
14
15
|
multiCMD = types.ModuleType("multiCMD")
|
|
15
16
|
sys.modules["multiCMD"] = multiCMD
|
|
16
17
|
_SRC_B85=r'''
|
|
17
|
-
rAT%)=o\O*k16n/!WXAE$ig8-O-i5'i
|
|
18
|
-
|
|
19
|
-
$
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
$
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
&
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
\
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
?
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
18
|
+
rAT%)=o\O*k16n/!WXAE$ig8-O-i5'i+r01HsH^aLdFUh,;[5]%l=/aQgL]knEMJj2Td\)1[`bH(.0)JfPPs3eIh)d^;Cl"iF+?;5$GY\@0&f>,"jBk/
|
|
19
|
+
YiH%6+MO0G/&_cVMbEIV%te4SsUIN@I9"V,DPig_^G):A?gs:G4WIo4U9V(ca0E&!d7=Dp]k!,Ha*Kah\qE4^ZV8^P$l@o]F\b'NLI.UoSLB2-BsAPK<
|
|
20
|
+
TC+)/F=9lUh$$,-L0U,\B$Beq)E4d*p3?al3[\ThsBL1lSo.bKBCPcZ<_?NmrNo)ZLj0,OEjn[/cE:Vptg0[Cq7Wfn#8`N8iGq@':[%E_d<[n<Da,s![
|
|
21
|
+
V)Tt+8:H>-U",1)'J#^*$DjC3hYHqso>I7'F)4%WckpDf5TQ[U>Pk,]P^C%M?]kDI/O16EW_.512+,N&D1#T:M<Me?X8f!KNEY"U?8EDPpeDJ;-.&W;?
|
|
22
|
+
-326YM;Q9M/&S.GMTP2c@aHA+L.,7!!]lK=G"2A9$Mn#i)90BAG\g`*=3<p@:IY(m8,"V^d3PJT@K]W@?A\!W6kH"hmEp.8DK*R_ohW,n+I7QR*Sfb+L
|
|
23
|
+
S5(D&1#C.%&r%+^HcL(J5*YnF0MA+4SHl8QPl4V@UCUFk<QC'@&7UcN*cu]g&F@0od<6jU``WZ,a:MLcSb8BA(%3QN`>>H+DC$,5K+8TK7<BO`@;aZ*[
|
|
24
|
+
?6L7CjnGCER'7]CXE_\_"<1M:InKkKGE>AlM%"EQ5P_'Y,t[&FsU<a/WOBAb4mdU+&c"$n^gPqMbZN?PoQE'gc^fHX_(R#,:;:[WKZ%<P53`fY>X>%24
|
|
25
|
+
u4,L;!3R>]4+n:7,9,VpcVc8_qP0=RNh2o\J+1jDoXmGcRr2YE4QRW;i7m8kmseZsr-"N>-E^_rhp(WM)\dh[M+`mX;]sW!dtj6bQ"$]rTjs71\k6@ik
|
|
26
|
+
V'EX!9N?-j"Fa'NG#r8T-L9QP3mSDPMkp2BpSM4>U/:.(mHbA+H)IeP9a4lAQmNT,S>j+(U`+.%p]g[_gP<ol;a?brX'Uhf9"@l_OA@(>>DF$Qp)q5<J
|
|
27
|
+
L4L%tG>WsXg0hYG8ODMFCBedZHSjg'7/('>=6^F2t[dT*0@XGshE*Qb">Zn@MEqNEA,W)Jhq.=:\DFJ$5X.P(2c0oWAM)sCQX4a+ppLYRu*C$a?-`)"t
|
|
28
|
+
heej,p1^sE4khB@M%@6Ao`I\PF5E6X@Km;Q>J4%gQXdf8rVp>X@e]/E&?u,o@mq!8N=S*?/9=H2GO+)g5%?*oAkVp_ba3V$Wiqn2ck^*?Y`*W:,%tRoj
|
|
29
|
+
2T"P*^jp:Z2%S2ocM95+GuT9D^C5?)k<)m?cFn4Xha^NL;Ym(2^uH%2R8e?,@_K%X6QSO.Kg@N[3B8(Xe_?SDDk><B5..I2r.b$"K!Ei'[A20o"QDaOW
|
|
30
|
+
4W4fGRBSGM>M^CddKF#LT_$eYk?70p+rq)ems(WKCVmWG>VPoai[<?h@T#feB`?+WkMqh,E-oQ`Ir>fol>(qYcpedQ1[Ul:=%Yj`kpmB!]$^fZZ@J']<
|
|
31
|
+
;4&ujr7fOlB@R@P>pf8]k7I-,i8gml7j.sEb4T=F3H/s9E47sPPU\o/JT\huWtebQVX9HYmD-eA)-IX>?OY)TpW2JL+S]`qtMP%MNM#fAG8*SmZd`:*e
|
|
32
|
+
),dK9+bNAL.IEWbsbk&Qd*6)`['\Bgk5XMQje1oqh*2D\@!CXk8[!"/:<^F$.?"m<QVPQs+s)Zc$n6+;Zr3UBma$.`0@*^8/')/tS*[Z$tqC*pH0$;8S
|
|
33
|
+
^gi9el!6%oqNHejC;*U)7))l1cOXd38%'Tp+WN=WI9QV'1@S;d'Y`(]S"iQ\.H4OVgFrjo_l0L=8"oA0-5'@$#Tjk2j@?IG/?GQB]rtATb#F'^>S+,eU
|
|
34
|
+
>EG^$+?01'Ntu#D`fM_,6=KQ&j9d>GI:j]U':WMe$<t%9H6LoqMJ^c@pb/(`DomO5Q#f8o@Z+/G2i)HnC[b2ElL)(=Yk/m__&QOhK_iE*%<b[XfaqBI!
|
|
35
|
+
o9&^DQ.nhOgI9`Y6V`D$Q<AjOEg=>M=TP44pk8R%\&J)Pc&23"I=.(k8g9931T.4DUP4h\2pOCG#WZV4)FNq5KZ\n+[eB;r?mU`\c;6(l!IQFeCJmc__
|
|
36
|
+
@Qr(';0WIY:j;u#<UUh&':1Y%PI'K)TD-2$3`0o?G7!deqi>OO-?X`,g?4DUY,o5;@;a&*\)p,Nk4OYHLR?m)`e/nPqS`Nbu;Tpd^Y"jErD10E5V:H*n
|
|
37
|
+
en;qR3^UkjT&)HY7H@[oe0@)=dg./)Q4>=g9c(,OjS>>ImG&Db%3:11Noe3[g%8p4(j)1Fnh',L*UZMaV'%Ea)Pd'*Bl/#KTMb)2p%gI@cFpR5MEslcf
|
|
38
|
+
\!#^uO3@eXeI/]0!+8;mY[c/4`]@8&7sH5fZ-,H@!78MQ7sa`l#m+&bHrj=.5^0Sc_hnQ.X6<S)]sl"W(Ji/a0ro#DBMdSaE9Uis#b.i]ITCL5#`oR7)
|
|
39
|
+
sU>*Ft]Q*QW/8%?mHW?OrYce_pSi#'*bDO<j=rH>dCMf[RPUgOZU(`DDu^+5,gh0qQp^uYKI0p9c;`Onho*d\CpR2dn3S"4]7KPREE5a<J&o(E"lUh&a
|
|
40
|
+
Jq%chOD1I_6"j>QNkWQYfPc:mVl-Kj/]^;-^O$kp`?1$g.MNRO!O>]Y-V4EYn>!/Ij4%i43\[Nd/NK65B\]Q+p?/%_u($_mdP5\I#Il35S5Z.*i="60i
|
|
41
|
+
Kg]Z1mc5)6AD3EDR->j8)N[qe4=eM?9C]%4W\0p3E95it<&#`)sk#HoZEHJIh!LecRIf9McY+^)m5#fI:0GqifK=PX3TjX9!#Z\-3>b[LP!+?)gJ>D2?
|
|
42
|
+
fN`u@pW2T5T*7]#7+B:*nk7DL/E6&XO_Nq;P>#&Hd6hcB"UZ9PN)t=sX%ZB$YI,@WO?o\B<?TR6;TL>R$g+@cPGjQmr199QeK4Kg]fS\YN!]-Si(BJk\
|
|
43
|
+
GanO#;c73L.XZA`_G4jNIo@<D4NVl<7O@&]EB?t<l)QjJ+L&!:VJ*-+Fe!].,$u7WkD4s4qOK;_BnumShGbhd!lFSj1CYc;M<iZVe,o^q@B`77p]aA*6
|
|
44
|
+
_M'5+tK,!]?!/s=inj$qj!Fpftfq!Nq?QZ\7sMrj67Wfr=Q?1pFP[r<Q%+u1-3bs!W*/$jp)Ep#3.Q=q+gQCmh9S?7(hp4JbbmH[brq4&^RpU^mT]gi<
|
|
45
|
+
'3bkdnnUNhZ<L\jo>p9H@F$=K!j1-2OK?;cKeL)sZc9d,a9%IBI1)EVkQsqos4"X>bh0KZS>=@G@pCa;*u6,p"maoU41F`;j?a(o\GQC`H/pfQ/2g0a7
|
|
46
|
+
SWU-ogXZLb+e$D7ToI8R^D7B9hT;:6Z[I_.CYlgXl#&@@t9Vr't)(7s13-'6ieK8CJDJ;LuWe`0cs,kp(n"&"pf=R0'q?pm&lbiRfu:$q=if#/YR0`?H
|
|
47
|
+
]F7!;R@n>QAmFCopc3/6ES6CR(PYfM"I4VdVP@I.P7m"@_-O?qkCXM^p(dm#7h&H1J<f8l7a0fabMSfWd_T[uBYB0St_#!)KY;N<N,Kd03a?87=.guQG
|
|
48
|
+
1TM6!9HN;u]0>F:]\%oH2082]@!k$ERMbu!Z&9bPF!H]Q8T(0G,sfi,.ORL66:"N-'CWYV$7+o=A+/'M1I-'XE9g0P\poHKSc.-0F0""Mphb7a0j9O>T
|
|
49
|
+
G=_,67d-Q"cGed2cnq%/uS7,iT2C^DJWCL&<9D&D.63ph.LMh/MKo)<1J_;T,/$sYGIFu7RrnIA&f5%*iR[iRops8>+IdJ&Wk);J"S2;fd`+_DJDWUg(
|
|
50
|
+
p3X,JdXLLF\SX(o$2\SS+nbh+P;W6-b-5N8O(11Jsg\b?Y3.OoBXdjZIZ$;b%84/'5jE#9nijE(`Q=0L9L`ViS7QT#3JjBTH%!/")19,pd%^kstW'dJI
|
|
51
|
+
$l^ILddi_JA"cCbRnr=nQ1[?q(KN7@-&ONu5VH'5Rp*Ql_(5</\Z*O_!H>`-4Q3b%)haV=:b.e]#cL?pT$#:Enf&ZoMrrSYYf^bu7uY:819Mt`gGWBB?
|
|
52
|
+
@&ZQK"dq(R_Ug1d!+YQkI>^0!\Og[gBM&\j`Qti/,_f"f+(V)i]B/<?W-]>9#'ff8@bB-,A;L)L>&9Tq;DEfXD_^_7I"aTNrQ<O3d5gr=?>dShPTt9TP
|
|
53
|
+
#gg#RD-GI\@Q#c-ELRE//Qo$'+^,c(2XG6]NNq2jCJoq2CLG3IJu!TiVQRBLpuKS.&V>6*AVm1:+=j5C]*5[b)Wm:*<6$WLjj^U&p8&@TQetCXW$+2_A
|
|
54
|
+
"SQrR8P=L+_/f'WZQICBNkQk9uM^Z\jZe/Rf.b$l"c"I>Wk\=X-PIt&Kc\2prh-Ce<bgk4us-Lc^&t#'U7bP%BETUNSPoH^1bhBEi3?)?%YL7PXDD'P>
|
|
55
|
+
):@]ZqUcO!DNn*9&qNq!s>KcJlGppY.hUc87IM]cmJ!#2*6;9FJod*r1EnO<`l0:;L;7:)4XqHgf3q7mKuG_H..Ch_U(kV]<;<C5kg3JN2h*aN+qtfde
|
|
56
|
+
/`KSiTliQAMaK(AfLkA-n&I,T11VIWHe?r;'19LK7SrcKil-j>Z?pH&_H)Xo_C*SIXFS,`>'`8=W)d"Z_"b@VHTRu#]*W4aZJ;Q/R]2N9Ma#$8ttU;,^
|
|
57
|
+
@XNk460U_;E"3htH"26=SRFph9WCq?;jEm=^l?Fbi:=^.O@G)^W&*ME:jkQIJ*JnDE)9.;BVHX$:]Q]K3oMNTTYH!tf,i!"K+!<</H!p-G'5saD6M&b8
|
|
58
|
+
1]^dhXZ>>mrf$G]E)D$]8LeUt02l83![>2Y+O7\68C`>5_Rc&3#-kA$n:R='],b>\@8:ns&+3X?Zr#Y1=5U`,c5I%5,)QLV4Wp:G5;jnimndrI<tIpJ6
|
|
59
|
+
$?biZk^Z:g>0+HK%#58D,?gQ88#uRY-iVs%]-m(6lgd0oFP[T-e:)#$-rRUIODoA!";!76[7M6jL(K*rA63^HXikX@Tj"?^4JU^%D'b>FV.u5.pEb`Z=
|
|
60
|
+
L)'(JXo>HE:M`2-qX9FA\44Kp"tD)(4"q8T>s37t7rITW_Y*-D<^<1g&qVUg%GBNs0gJaK4.^B0#04Cll'2C=Nl?H3ch!KBq3-'Dg'qL#]j(j_%6%c*\
|
|
61
|
+
g>Gfl!0=\MY=1\Z8NcJP>eYpEaA<gLcU(RL9$TKU)Z2?\B\L.jKNB3V0L,]2s8H/jce_@aBfc!@HVJtj-n#Pp2kl;oZrYG-b)Djn/H0?:UH<nM>U42-Y
|
|
62
|
+
qM3H(BPUsE<7/Nq`:JOG5o3j&I0o\+J)#ugSX))AU$q)THd<7L23&u2IH1TJ=QQ*4?B'8iO5otnQ`@0]?1Kf>`Z0jgR@Sm?ZMZ(+T2l:K$Fpbt_O^YSo
|
|
63
|
+
^/G7BL#o4CC-I=bB&$FR<)Vc;b?qr^B[=,F2.HX%U5uWmR6S(pF7dN9]IZ7;R/7`J\M*G_l68BoL_N[I\-7]6dN0P_;H@,T'2hu''-<bAm_EO;5?`\ti
|
|
64
|
+
OXLS>]P;b["Ul2QgR(n_bXoGh?#;=0aCYO+7Mki-E7a'Uh@"5q[-TP=D#%C3d%)hAs0#;ZY-K*3Cdf':4o"R>J7ilWAgq4E\tasQ--8D"']pa_j,)PkM
|
|
65
|
+
bsm^=CfQ71AutU;t=GbV+)dl40bjFQWCB.*8G_,,@c0%KolE%NjI[o6=XEJI8ZE,7su!_o<.D2KP20*bJM03UHLGM]jk#j&I,Do[_)0i/Sk+?dTG6NCf
|
|
66
|
+
^(R$Ci_8i[6\aPCf64<TGpi>a7Ib!6oeoFq6h>$aE[gQ24is"<M0ShoMUM)GVS#$\$15U4i0PRsD6dO0sU0.q)$bI1u,o6l&kPfW?"p>_SARL`\PR?1R
|
|
67
|
+
5o@Pl"!NtJenU+98.k/r5U1UZ7M,/T3GSG90Nq.+AZtd>oDmre2bS&hnW''H9=OLGSb(DN+$_agg+BPAU+Z$P@Y#0u`b!`BqjK(c-M3'8B]mU*GZ^EY2
|
|
68
|
+
,bV727dI"NRl[93o'S*8K@R>-,J.mL8K>t0:Fk5NBr"7)QVGM:p_L(N+TtR/^uHfqqZ'F+*u%Q1*8GT4oZ)SY79/=LSO]F1>f)t<Eu7g$`"(&1k5RZ?`
|
|
69
|
+
d,bF_s`rs`l<=NS.$Y!D@keL*pAHGM9Fsr48..sfP+2>;'1%:;77EEB+Rr'/]0SN_\4NZjP"l]a#>t"=Fq.?7BGEM.Uf*ZXA9<k)rlUg(6@'DdkDF3F1
|
|
70
|
+
+)qm(DCUmN1l25O[S]%:12CY3ER"afKn"(OGP[?lNiNVuH/O^`E9O0`N(t(mEK;giRBoa87G:/0L"P?MZoel.bRg*JfYY%)61]'^Xtc$k,X\L.#-=;_3
|
|
71
|
+
OMHk:(I1_&;K"bU_UYj.r:aV]=r(,"`V@/&%B]8^a6AN5kei_"p5T6kYURGOZ]s,j_J87k$HXY2\qhiK#F>WA-kp&tLioJM&;]KCLF48B)EI'+lpqd>/
|
|
72
|
+
nF_lifh'39C7qV=`g!k?i+/B.+g+@@%c%rfrAP;oGAnPl(`_*qaF9!fg<itd`&Xt;Q(S#:m>1W!OE>*OfN^fZ<KG"`c)u+LC/Gq^\K/@]^W\SRa4Y$Am
|
|
73
|
+
hMeg"0t=JoqpXRl`E`K[-kh@nOQEp_#pbbIYS#F\&GZ^ejs))]CAjm#5tOF,X5R5SIbL_A0gn,((P?D5@?npXKK[d2q;t1\EGD*g+\7B%Kdm9,<;`d',
|
|
74
|
+
><$A3T@Q&U0^@&8(:X?U-#h%DCd4>a0Z[8`CBqk=9L>B_W+AkmfCM^RS?.A0UQ/;lTa]N>j;^n6N:Z\OO<"kHI8aeV\Y"HGZ;XE5Du`58VR"u!Q];XI`
|
|
75
|
+
"%2,:u%l`eVtimgc`j+A:D*YRl1JTt+I;EF[b"0_oH+rQ,gaI!WHg5J8?(_WkT7,V+I).+JP:>6u?_UDXrt-Ro`NO&X<,Q.bqd*tJ%kqIf?trIe.!^Y'
|
|
76
|
+
Bms+miQk<;!&"[mZj_V"c\->3QYd`N6^C0`FS5.8IZ;CbqVS*K8`_ioo!1CLRKdG2b"3*g!ML.H/;mpXO%S@IF^KE=J"L\DgVO)fdHk,cmSWL!1LnXuo
|
|
77
|
+
KaF.9D;Q%^XMP1:PGSOd-;g0]foW%+3/ttgCQ7c0&jDSs_6mt-9i1%&WpRr2K5q6%A$L]d.7Xr&.HDH,]rDMEVs0q9h$0NW.`JtJ8^k1<<Q]dd``d_F-
|
|
78
|
+
gP'Gf]t>8TWaX:RP?Xt1YTAfneY<D*5K8\<E*J>u5J^]fI/'cagZ)_Z&U^n(/pHePI*6DhOqMC!/D,lXR0ViO#k]2s,/9/?"9qe6fTQcJ_qLb5L`_e"f
|
|
79
|
+
J*%>/D)d*8*qaEEfcI//s\P"(OlN$a=;ntrQIS2KM$s.a=Dqfo;Fm;qpWGYVcKQlqVSlZ)8..<kcWQSb5bi^#Y4drENObuIf*+d2?1h.K%^10cukEoSV
|
|
80
|
+
F,Y@hFbHH$%O$JDU.;B&U)AIJ>NX8J7cl'qJ3\D`^*[Um5=(G^0Z>DkGlGo_Nn!&!;7'/H2r0Y)n)nJnMtHf0!P=hf@TE&m(Ip\>^sR;l#K5M^)iZAZ4
|
|
81
|
+
*k?AK[G#h'@L,?.hekCiJ#BO-[lZogLl`T$S^Dg$_-9XVM!pbPMANER2`9H$F-C_ZWi;)i"rIY\\7p@;GLF,Hg*Z]A=I]eF7_q=jV<G=Grl4)fo3=<uY
|
|
82
|
+
4/#g=<ObXqc?D=>-,,9f/UkV6/h81L]:+8SFBsr\9U"#EM:oG]MUcm5kH[8IiS[i.#A[`?aA:0:>;#<pDK5)l^fWjY<$To:XMFt#Sei=Y?Kc"YdA[)56
|
|
83
|
+
gT=%0o(3^JrY0c1bQRqMY;Z)go$c^&VW6c\CNg#&XPrh(nB4u+ltW<J%X']qV]Fc`aU1tEc(V#-\V(CqS1b2GR<D=iZSO$d/'gJJ5kh?<4$.$`IbPe:R
|
|
84
|
+
5==iH?U&__&Utl6cE9F5frdBXI#NHUW(JlH\Y$[\HaBe@t).qlC61b9c=8-T;H0&-.`==Jt,Dq_1FK^[j,Jdho@e;KuPm8oG%)X@)&\A.gX6l%:^4,kO
|
|
85
|
+
'#^"_CkcLUm6M&t2^`>\b6B*;gMY]<W<0Cq7111'+41eknqg,,F;B=)]8!@AaH_<r?4T>D;EbYm&FWQSC^q$iWNsBZG@a4hf1;8F_P3:)+&F&B^.EL;f
|
|
86
|
+
(AdhP2#WF)l`<#bE0\#&IJCU$1K_'K^^4L8Js!3nX?2#@F-eqL.XfJItb64ia\J-$"C;c%)71P0fJ4psK$Yiq]F3.:N;W<6@c/bZ?(-I?Q>]l,T[gRfh
|
|
87
|
+
.A0kNfUa.eGEULKtA[_gU#VT.l=]kgfo(uUkHW2^E=jkRS\LkO.+E`E3p&JaZ4e^7Q]c;bj\TiI0H\sETXLMu!_WF(>baf(ZHZ+?T%UL54W'h<[BpUD9
|
|
88
|
+
9jIOLDC$]\k+(9r?cCS#lLXJ`?Mb*Dk3M\d%5-c[1e81Cl*[O&?p@?H6]1a^,,N"%J>DRQSd$Uj=mdq_Z$rd>hAnG2O9!6-A7f+M[j"s#&=#!CFbYtrM
|
|
89
|
+
'mo-fGT7JTg>^8#(%>pM6^#DHnP]!YbT89j/m=!Z*Z_S]bZGelFlS*&O;RJ'E:.X3=4:E\@o5Kb@fCYN]h=b!t1Nk3rTV)QCpk/*]+.M(KAQM_*6.l\!
|
|
90
|
+
`ja9_eH()4*&(0G0oJ]*\1H/t*nLflr_Bqj#PLODig\;1^%i1BWMa<U_gQAr+hOP7kcC/MKg3NG]L974)8t,f<b)J"KV\i,c2[\C6BO'VL2@FI(+@]5'
|
|
91
|
+
S@/Mnef:&L'p9;3O\F0,u=(@(>apt#?($GBL!Ya:M%[$i*`NG#'!\),TR+Bh4YTl%h!#iMbIPho%7`Xn/jB@E&n?TYaS)K/G;VqrL^pekNYA'Ll4ZD3R
|
|
92
|
+
`ktS[c%r8d.8LGiFGX<i\JZf)7?<S7?@rZ]_%EVeC8nQFF`;E%[eu,<kT9SIV<'!]dFPVL$_EWD-mEA/m.soZ.g+);q%jDSu(QbElb&3K^^chec1$ieA
|
|
93
|
+
Htk:%*$g=ji0G$.dnNRE#UH:<DZu9f'a27Rmto0QF!b(WrRO=(q,,N5C=hZ@bgJF&>a5R@ldX/I<Pq155]n<FKQ=&&4@nZ*hqUCFJ`DT=RT.t`LdrO8X
|
|
94
|
+
sl'PrQb)rQW,QAmC[DmhX._[cOr1E0<VMP1KLip8Y.qWYMfY7-Sqm"o(>iTp#:lPT"Zc1Ud6S9Y/X.`bBTRZ/d4sNI*Wi@O4=U[W+$KE72+S_o@eY"P\
|
|
95
|
+
@^.Q2gDB<;!P`&U-_o?>lAQ)L.$D/o.`r3THQP2RqTPLe65RPtsF]c"[FtoZM%S"tC[)7tq8-T<l/9KH(g1%gM<\*cB*BgXL`?+/s]]#264p5^rQ:s)U
|
|
96
|
+
"qbj;Mie!Rq;`87n!Q-D0i?4h=q6[=_YlbC`NO]\luLcY%3:gV(2k45fipEmpsT0)R77.cfeXNp<,.TfG-3Q+a%6mXViRT7>ikIL-`fJ#*qb&u?_CAi$
|
|
97
|
+
YHIEt9+A$7XXi=`TDL><snVAZ-KZ->p#/";eho+]3.PE%[JRQLcaQ2)B1L?(fn1@_l8/h`#X/>kZaB(?+igB9!$g#U%q9)\jK0d%!\DPII1E+YB%fZ*l
|
|
98
|
+
_'&W*QPn=VO8T.q`Wd`C/1OStg5GPj5%\'hR5nmcnF##s#ZdZ5\!lnhnmLWPOYg/.XJ@G>--rf7BF)<tAAN9aiFcf^0/63F_6Pt;/i\4=A3kNg57HVuS
|
|
99
|
+
Bk?Ia#@D8h35UhPh20m7(MP*I@JEIp^9h!.t1]T_>K,&;>*sZ00DI;`;FHVIZMCG.f<Tu>f,C3E2M2(20'qMW>m[`/kX7<-\gtFs";4*E"7!u!HnI-I\
|
|
100
|
+
:CrXnN2=$WC7hT2VjPN>-/ncqs(e1C=OBQfuIiRIq]Od[O3nWm4X0`5UfH^2Mqe)%2DhPmmU+m@UW+I\"g?kuEGGG0?(R[c9?,o1=MKmu:'b';u%dnld
|
|
101
|
+
hC&:PC:E%[FANK#cH#@/_I>:!&/)=\B\c="2%19!$[Zl]el65mfBHS+OJM(bb*[5$&m0lZ\p2AiZ-`3@2IfRBSt^pl,N8\=/q[nfdI@HP).o0T2USWJo
|
|
102
|
+
#-H=)K\O@*IBaYQY*5)<QXXiqV0<g`TS`9^5\A`eL3h(@';$d4&U,IgcO]eX`!!/HMqptiKQG749Z,C;P!WW3#!!HG.'''
|
|
94
103
|
exec(lzma.decompress(base64.a85decode(_SRC_B85)).decode("utf-8"), multiCMD.__dict__)
|
|
95
104
|
|
|
96
|
-
version='0.
|
|
105
|
+
version='0.32'
|
|
97
106
|
__version__ = version
|
|
98
107
|
|
|
99
108
|
# Colors
|
|
@@ -102,22 +111,27 @@ GREEN = "\033[92m"
|
|
|
102
111
|
YELLOW = "\033[93m"
|
|
103
112
|
RESET = "\033[0m"
|
|
104
113
|
|
|
105
|
-
USE_SUDO =
|
|
106
|
-
|
|
107
|
-
USE_SUDO = False
|
|
114
|
+
USE_SUDO = os.geteuid() != 0
|
|
115
|
+
_SUDO_CHECKED = False
|
|
108
116
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
def ensure_privileges():
|
|
118
|
+
"""Fail fast if non-root and sudo is unavailable. Safe to call repeatedly."""
|
|
119
|
+
global _SUDO_CHECKED
|
|
120
|
+
if not USE_SUDO or _SUDO_CHECKED:
|
|
121
|
+
return
|
|
122
|
+
_SUDO_CHECKED = True
|
|
123
|
+
if multiCMD.run_command(['which', 'sudo'], quiet=True, return_code_only=True):
|
|
112
124
|
print(f"{RED}sudo is required to run this script.{RESET}")
|
|
113
125
|
sys.exit(1)
|
|
114
126
|
|
|
115
127
|
def execute_command(command,wait=True):
|
|
128
|
+
ensure_privileges()
|
|
116
129
|
if USE_SUDO:
|
|
117
130
|
command = ['sudo'] + command
|
|
118
131
|
return multiCMD.run_command(command,quiet=True,wait_for_return=wait)
|
|
119
132
|
|
|
120
133
|
def execute_commands(commands,wait=True):
|
|
134
|
+
ensure_privileges()
|
|
121
135
|
if USE_SUDO:
|
|
122
136
|
commands = [['sudo']+command for command in commands]
|
|
123
137
|
return multiCMD.run_commands(commands,quiet=True,wait_for_return=wait,max_threads=0)
|
|
@@ -154,13 +168,15 @@ def parse_show_info(info):
|
|
|
154
168
|
return uuid, fs_used, device_path
|
|
155
169
|
|
|
156
170
|
def parse_device_stats(stats):
|
|
171
|
+
# Multi-device filesystems print one block per device; sum each counter across devices.
|
|
157
172
|
err_dict = {}
|
|
158
173
|
for line in stats:
|
|
159
174
|
parts = line.split()
|
|
160
|
-
if len(parts) != 2:
|
|
175
|
+
if len(parts) != 2 or not parts[1].isdigit():
|
|
161
176
|
continue
|
|
162
177
|
key, val = parts
|
|
163
|
-
|
|
178
|
+
key = key.split(".")[-1]
|
|
179
|
+
err_dict[key] = err_dict.get(key, 0) + int(val)
|
|
164
180
|
return err_dict
|
|
165
181
|
|
|
166
182
|
def color_error_count(error_count):
|
|
@@ -320,7 +336,7 @@ def main():
|
|
|
320
336
|
|
|
321
337
|
|
|
322
338
|
while True:
|
|
323
|
-
header = ["Mount", "Path", "FS Used", "w_err", "r_err", "bit_rot", "gen_err",
|
|
339
|
+
header = ["Mount", "Path", "FS Used", "w_err", "r_err", "f_err", "bit_rot", "gen_err",
|
|
324
340
|
"scrub_status", "scrub_time", "scrub_rate", "scrubbed_data", "error_summary"]
|
|
325
341
|
table = []
|
|
326
342
|
running_scrubs = []
|
|
@@ -356,6 +372,7 @@ def main():
|
|
|
356
372
|
fs_used,
|
|
357
373
|
color_error_count(err_dict.get('write_io_errs', 0)),
|
|
358
374
|
color_error_count(err_dict.get('read_io_errs', 0)),
|
|
375
|
+
color_error_count(err_dict.get('flush_io_errs', 0)),
|
|
359
376
|
color_error_count(err_dict.get('corruption_errs', 0)),
|
|
360
377
|
color_error_count(err_dict.get('generation_errs', 0)),
|
|
361
378
|
scrub_data["scrub_status"],
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import statbtrfs
|
|
4
|
+
from fakes import (
|
|
5
|
+
MOUNT_LINES_MULTI,
|
|
6
|
+
MOUNT_LINES_SINGLE,
|
|
7
|
+
SCRUB_FINISHED_OK,
|
|
8
|
+
SHOW_MULTI_DEVICE,
|
|
9
|
+
SHOW_SINGLE,
|
|
10
|
+
STATS_MULTI_DEVICE,
|
|
11
|
+
STATS_ZERO,
|
|
12
|
+
CommandMap,
|
|
13
|
+
install_command_map,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _status_map_for_mounts(mount_lines, mounts_show_stats_scrub):
|
|
18
|
+
"""Build CommandMap for mount + per-mount show/stats/scrub triples."""
|
|
19
|
+
mapping = {("mount", "-t", "btrfs"): mount_lines}
|
|
20
|
+
# execute_commands looks up each subcommand individually via _lookup
|
|
21
|
+
for mount, (show, stats, scrub) in mounts_show_stats_scrub.items():
|
|
22
|
+
mapping[("btrfs", "filesystem", "show", mount)] = show
|
|
23
|
+
mapping[("btrfs", "device", "stats", mount)] = stats
|
|
24
|
+
mapping[("btrfs", "scrub", "status", mount)] = scrub
|
|
25
|
+
return CommandMap(mapping=mapping)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_main_no_mounts_message(monkeypatch, capsys):
|
|
29
|
+
cm = CommandMap(default=[])
|
|
30
|
+
install_command_map(monkeypatch, cm, statbtrfs)
|
|
31
|
+
monkeypatch.setattr(statbtrfs.sys, "argv", ["statbtrfs"])
|
|
32
|
+
statbtrfs.main()
|
|
33
|
+
assert "No Btrfs mounts found." in capsys.readouterr().out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_main_status_prints_table_once(monkeypatch, capsys):
|
|
37
|
+
cm = _status_map_for_mounts(
|
|
38
|
+
MOUNT_LINES_SINGLE,
|
|
39
|
+
{"/mnt/a": (SHOW_SINGLE, STATS_ZERO, SCRUB_FINISHED_OK)},
|
|
40
|
+
)
|
|
41
|
+
install_command_map(monkeypatch, cm, statbtrfs)
|
|
42
|
+
monkeypatch.setattr(statbtrfs.multiCMD, "pretty_format_table", lambda rows, header=None: "TABLE")
|
|
43
|
+
monkeypatch.setattr(statbtrfs.sys, "argv", ["statbtrfs"])
|
|
44
|
+
sleeps = []
|
|
45
|
+
monkeypatch.setattr(statbtrfs.time, "sleep", lambda s: sleeps.append(s))
|
|
46
|
+
statbtrfs.main()
|
|
47
|
+
out = capsys.readouterr().out
|
|
48
|
+
assert "TABLE" in out
|
|
49
|
+
assert sleeps == [] # no -s => single pass, no sleep
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_main_pattern_filters_mounts(monkeypatch, capsys):
|
|
53
|
+
cm = _status_map_for_mounts(
|
|
54
|
+
MOUNT_LINES_MULTI,
|
|
55
|
+
{
|
|
56
|
+
"/data": (SHOW_SINGLE, STATS_ZERO, SCRUB_FINISHED_OK),
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
install_command_map(monkeypatch, cm, statbtrfs)
|
|
60
|
+
monkeypatch.setattr(statbtrfs.multiCMD, "pretty_format_table", lambda rows, header=None: f"ROWS={len(rows)}")
|
|
61
|
+
monkeypatch.setattr(statbtrfs.sys, "argv", ["statbtrfs", "data"])
|
|
62
|
+
statbtrfs.main()
|
|
63
|
+
assert "ROWS=1" in capsys.readouterr().out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_main_table_shows_errors_from_every_device(monkeypatch, capsys):
|
|
67
|
+
cm = _status_map_for_mounts(
|
|
68
|
+
MOUNT_LINES_SINGLE,
|
|
69
|
+
{"/mnt/a": (SHOW_MULTI_DEVICE, STATS_MULTI_DEVICE, SCRUB_FINISHED_OK)},
|
|
70
|
+
)
|
|
71
|
+
install_command_map(monkeypatch, cm, statbtrfs)
|
|
72
|
+
captured = {}
|
|
73
|
+
|
|
74
|
+
def fake_table(rows, header=None):
|
|
75
|
+
captured["rows"], captured["header"] = rows, header
|
|
76
|
+
return "TABLE"
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(statbtrfs.multiCMD, "pretty_format_table", fake_table)
|
|
79
|
+
monkeypatch.setattr(statbtrfs.sys, "argv", ["statbtrfs"])
|
|
80
|
+
statbtrfs.main()
|
|
81
|
+
(row,) = captured["rows"]
|
|
82
|
+
cells = dict(zip(captured["header"], row))
|
|
83
|
+
assert cells["w_err"] == statbtrfs.color_error_count(7)
|
|
84
|
+
assert cells["r_err"] == statbtrfs.color_error_count(1)
|
|
85
|
+
assert cells["f_err"] == statbtrfs.color_error_count(2)
|
|
86
|
+
assert cells["bit_rot"] == statbtrfs.color_error_count(42)
|
|
87
|
+
assert cells["gen_err"] == statbtrfs.color_error_count(0)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_main_version(monkeypatch):
|
|
91
|
+
monkeypatch.setattr(statbtrfs.sys, "argv", ["statbtrfs", "-V"])
|
|
92
|
+
with pytest.raises(SystemExit) as ei:
|
|
93
|
+
statbtrfs.main()
|
|
94
|
+
assert ei.value.code == 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_main_exclude_recent_prints_skip(monkeypatch, capsys):
|
|
98
|
+
scrub_epoch = statbtrfs.parse_scrub_time_to_epoch("Thu May 2 09:56:55 2024")
|
|
99
|
+
monkeypatch.setattr(statbtrfs.time, "time", lambda: scrub_epoch + 24 * 3600)
|
|
100
|
+
|
|
101
|
+
mapping = {
|
|
102
|
+
("mount", "-t", "btrfs"): MOUNT_LINES_SINGLE,
|
|
103
|
+
("btrfs", "scrub", "status", "/mnt/a"): list(SCRUB_FINISHED_OK),
|
|
104
|
+
}
|
|
105
|
+
cm = CommandMap(mapping=mapping, default=[])
|
|
106
|
+
install_command_map(monkeypatch, cm, statbtrfs)
|
|
107
|
+
monkeypatch.setattr(statbtrfs.sys, "argv", ["statbtrfs", "-d", "7"])
|
|
108
|
+
statbtrfs.main()
|
|
109
|
+
out = capsys.readouterr().out
|
|
110
|
+
assert "Skipping mounts scrubbed within the last 7d" in out
|
|
111
|
+
assert "/mnt/a" in out
|
|
112
|
+
assert "No Btrfs mounts found." in out
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import statbtrfs
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_color_error_count_thresholds():
|
|
5
|
+
assert statbtrfs.GREEN in statbtrfs.color_error_count(0)
|
|
6
|
+
assert statbtrfs.YELLOW in statbtrfs.color_error_count(1)
|
|
7
|
+
assert statbtrfs.YELLOW in statbtrfs.color_error_count(9)
|
|
8
|
+
assert statbtrfs.RED in statbtrfs.color_error_count(10)
|
|
9
|
+
assert "0" in statbtrfs.color_error_count(0)
|
|
10
|
+
assert "12" in statbtrfs.color_error_count(12)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import statbtrfs
|
|
4
|
+
from fakes import SCRUB_FINISHED_OK, SCRUB_NEVER, SCRUB_RUNNING, CommandMap, install_command_map
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_filter_recent_days_nonpositive_noop(monkeypatch):
|
|
8
|
+
mounts = {"/mnt/a", "/mnt/b"}
|
|
9
|
+
kept, skipped = statbtrfs.filter_recently_scrubbed_mounts(mounts, 0)
|
|
10
|
+
assert kept == mounts
|
|
11
|
+
assert skipped == []
|
|
12
|
+
kept, skipped = statbtrfs.filter_recently_scrubbed_mounts(mounts, -1)
|
|
13
|
+
assert kept == mounts
|
|
14
|
+
assert skipped == []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_filter_recent_skips_finished_within_window(monkeypatch):
|
|
18
|
+
# SCRUB_FINISHED_OK time is May 2 2024 — freeze "now" to a few days later
|
|
19
|
+
scrub_epoch = statbtrfs.parse_scrub_time_to_epoch("Thu May 2 09:56:55 2024")
|
|
20
|
+
monkeypatch.setattr(statbtrfs.time, "time", lambda: scrub_epoch + 2 * 24 * 3600)
|
|
21
|
+
|
|
22
|
+
def fake_exec(command, wait=True):
|
|
23
|
+
return list(SCRUB_FINISHED_OK)
|
|
24
|
+
|
|
25
|
+
monkeypatch.setattr(statbtrfs, "execute_command", fake_exec)
|
|
26
|
+
kept, skipped = statbtrfs.filter_recently_scrubbed_mounts({"/mnt/a"}, 7)
|
|
27
|
+
assert kept == set()
|
|
28
|
+
assert skipped == ["/mnt/a"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_filter_recent_keeps_old_finished(monkeypatch):
|
|
32
|
+
scrub_epoch = statbtrfs.parse_scrub_time_to_epoch("Thu May 2 09:56:55 2024")
|
|
33
|
+
monkeypatch.setattr(statbtrfs.time, "time", lambda: scrub_epoch + 30 * 24 * 3600)
|
|
34
|
+
monkeypatch.setattr(statbtrfs, "execute_command", lambda *a, **k: list(SCRUB_FINISHED_OK))
|
|
35
|
+
kept, skipped = statbtrfs.filter_recently_scrubbed_mounts({"/mnt/a"}, 7)
|
|
36
|
+
assert kept == {"/mnt/a"}
|
|
37
|
+
assert skipped == []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_filter_recent_keeps_never_and_running(monkeypatch):
|
|
41
|
+
responses = {
|
|
42
|
+
"/mnt/never": SCRUB_NEVER,
|
|
43
|
+
"/mnt/run": SCRUB_RUNNING,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def fake_exec(command, wait=True):
|
|
47
|
+
mount = command[-1]
|
|
48
|
+
return list(responses[mount])
|
|
49
|
+
|
|
50
|
+
monkeypatch.setattr(statbtrfs, "execute_command", fake_exec)
|
|
51
|
+
kept, skipped = statbtrfs.filter_recently_scrubbed_mounts({"/mnt/never", "/mnt/run"}, 7)
|
|
52
|
+
assert kept == {"/mnt/never", "/mnt/run"}
|
|
53
|
+
assert skipped == []
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import statbtrfs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_module_imports_without_exiting():
|
|
7
|
+
assert hasattr(statbtrfs, "version")
|
|
8
|
+
assert callable(statbtrfs.main)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_ensure_privileges_exits_when_sudo_missing(monkeypatch, capsys):
|
|
12
|
+
monkeypatch.setattr(statbtrfs, "USE_SUDO", True)
|
|
13
|
+
monkeypatch.setattr(statbtrfs, "_SUDO_CHECKED", False)
|
|
14
|
+
|
|
15
|
+
def fake_run(cmd, **kwargs):
|
|
16
|
+
# which sudo fails -> non-zero truthy return_code_only
|
|
17
|
+
return 1
|
|
18
|
+
|
|
19
|
+
monkeypatch.setattr(statbtrfs.multiCMD, "run_command", fake_run)
|
|
20
|
+
with pytest.raises(SystemExit) as ei:
|
|
21
|
+
statbtrfs.ensure_privileges()
|
|
22
|
+
assert ei.value.code == 1
|
|
23
|
+
assert "sudo is required" in capsys.readouterr().out
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_ensure_privileges_noop_when_root(monkeypatch):
|
|
27
|
+
monkeypatch.setattr(statbtrfs, "USE_SUDO", False)
|
|
28
|
+
monkeypatch.setattr(statbtrfs, "_SUDO_CHECKED", False)
|
|
29
|
+
calls = []
|
|
30
|
+
monkeypatch.setattr(
|
|
31
|
+
statbtrfs.multiCMD,
|
|
32
|
+
"run_command",
|
|
33
|
+
lambda *a, **k: calls.append(1) or 0,
|
|
34
|
+
)
|
|
35
|
+
statbtrfs.ensure_privileges()
|
|
36
|
+
assert calls == []
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import statbtrfs
|
|
4
|
+
from conftest import skip_unless_live_btrfs
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.live
|
|
8
|
+
def test_live_status_table(monkeypatch, capsys):
|
|
9
|
+
skip_unless_live_btrfs()
|
|
10
|
+
monkeypatch.setattr(statbtrfs.sys, "argv", ["statbtrfs"])
|
|
11
|
+
# Safety: if something patched scrub in, fail hard
|
|
12
|
+
real_exec = statbtrfs.execute_command
|
|
13
|
+
|
|
14
|
+
def guard(command, wait=True):
|
|
15
|
+
if list(command)[:3] == ["btrfs", "scrub", "start"] or list(command)[:4] == [
|
|
16
|
+
"sudo",
|
|
17
|
+
"btrfs",
|
|
18
|
+
"scrub",
|
|
19
|
+
"start",
|
|
20
|
+
]:
|
|
21
|
+
raise AssertionError("live tests must not scrub start")
|
|
22
|
+
return real_exec(command, wait=wait)
|
|
23
|
+
|
|
24
|
+
monkeypatch.setattr(statbtrfs, "execute_command", guard)
|
|
25
|
+
# execute_commands used for status; wrap similarly
|
|
26
|
+
real_execs = statbtrfs.execute_commands
|
|
27
|
+
|
|
28
|
+
def guard_many(commands, wait=True):
|
|
29
|
+
for command in commands:
|
|
30
|
+
if list(command)[:3] == ["btrfs", "scrub", "start"]:
|
|
31
|
+
raise AssertionError("live tests must not scrub start")
|
|
32
|
+
return real_execs(commands, wait=wait)
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr(statbtrfs, "execute_commands", guard_many)
|
|
35
|
+
statbtrfs.main()
|
|
36
|
+
out = capsys.readouterr().out
|
|
37
|
+
assert "Mount" in out or "scrub_status" in out or "TABLE" in out or "│" in out or "|" in out
|
|
38
|
+
# Header fields appear inside pretty_format_table output; require at least one known column name:
|
|
39
|
+
assert any(h in out for h in ("Mount", "scrub_status", "FS Used", "w_err"))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import statbtrfs
|
|
2
|
+
from fakes import MOUNT_LINES_MULTI, CommandMap, install_command_map
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_find_btrfs_mounts_empty(monkeypatch):
|
|
6
|
+
cm = CommandMap(default=[])
|
|
7
|
+
install_command_map(monkeypatch, cm, statbtrfs)
|
|
8
|
+
assert statbtrfs.find_btrfs_mounts([]) == set()
|
|
9
|
+
assert statbtrfs.find_btrfs_mounts(None) == set()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_find_btrfs_mounts_none_output(monkeypatch):
|
|
13
|
+
monkeypatch.setattr(statbtrfs, "execute_command", lambda *a, **k: None)
|
|
14
|
+
assert statbtrfs.find_btrfs_mounts([]) == set()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_find_btrfs_mounts_all(monkeypatch):
|
|
18
|
+
cm = CommandMap({("mount", "-t", "btrfs"): MOUNT_LINES_MULTI})
|
|
19
|
+
install_command_map(monkeypatch, cm, statbtrfs)
|
|
20
|
+
assert statbtrfs.find_btrfs_mounts([]) == {"/mnt/a", "/mnt/b", "/data"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_find_btrfs_mounts_pattern_filter(monkeypatch):
|
|
24
|
+
cm = CommandMap({("mount", "-t", "btrfs"): MOUNT_LINES_MULTI})
|
|
25
|
+
install_command_map(monkeypatch, cm, statbtrfs)
|
|
26
|
+
assert statbtrfs.find_btrfs_mounts(["mnt"]) == {"/mnt/a", "/mnt/b"}
|
|
27
|
+
assert statbtrfs.find_btrfs_mounts(["data"]) == {"/data"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
import statbtrfs
|
|
4
|
+
from fakes import (
|
|
5
|
+
SCRUB_FINISHED_ERRORS,
|
|
6
|
+
SCRUB_FINISHED_OK,
|
|
7
|
+
SCRUB_NEVER,
|
|
8
|
+
SCRUB_RUNNING,
|
|
9
|
+
SCRUB_TIME_BASIC,
|
|
10
|
+
SCRUB_TIME_ISO,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_parse_scrub_never_defaults():
|
|
15
|
+
data = statbtrfs.parse_scrub_status(SCRUB_NEVER)
|
|
16
|
+
assert data["scrub_status"] == "Never"
|
|
17
|
+
assert data["scrub_time"] == "N/A"
|
|
18
|
+
assert data["scrub_rate"] == "N/A"
|
|
19
|
+
assert data["scrubbed_data"] == "N/A"
|
|
20
|
+
assert data["error_summary"] == "N/A"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_parse_scrub_empty_defaults():
|
|
24
|
+
data = statbtrfs.parse_scrub_status([])
|
|
25
|
+
assert data["scrub_status"] == "Never"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_parse_scrub_finished_ok_uses_total_to_scrub():
|
|
29
|
+
data = statbtrfs.parse_scrub_status(SCRUB_FINISHED_OK)
|
|
30
|
+
assert data["scrub_status"] == "finished"
|
|
31
|
+
assert data["scrub_time"] == "Thu May 2 09:56:55 2024"
|
|
32
|
+
assert data["scrub_rate"] == "17.00MiB/s"
|
|
33
|
+
assert data["scrubbed_data"] == "10.00GiB"
|
|
34
|
+
assert "OK" in data["error_summary"]
|
|
35
|
+
assert statbtrfs.GREEN in data["error_summary"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_parse_scrub_running_uses_bytes_scrubbed():
|
|
39
|
+
data = statbtrfs.parse_scrub_status(SCRUB_RUNNING)
|
|
40
|
+
assert data["scrub_status"] == "running"
|
|
41
|
+
assert data["scrubbed_data"] == "40.00GiB"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_parse_scrub_finished_with_errors_is_red():
|
|
45
|
+
data = statbtrfs.parse_scrub_status(SCRUB_FINISHED_ERRORS)
|
|
46
|
+
assert "csum=3" in data["error_summary"]
|
|
47
|
+
assert statbtrfs.RED in data["error_summary"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_parse_scrub_time_formats():
|
|
51
|
+
epoch = statbtrfs.parse_scrub_time_to_epoch(SCRUB_TIME_BASIC)
|
|
52
|
+
assert epoch == datetime.strptime(SCRUB_TIME_BASIC, "%a %b %d %H:%M:%S %Y").timestamp()
|
|
53
|
+
epoch_iso = statbtrfs.parse_scrub_time_to_epoch(SCRUB_TIME_ISO)
|
|
54
|
+
assert epoch_iso == datetime.strptime(SCRUB_TIME_ISO, "%Y-%m-%d %H:%M:%S").timestamp()
|
|
55
|
+
assert statbtrfs.parse_scrub_time_to_epoch("N/A") is None
|
|
56
|
+
assert statbtrfs.parse_scrub_time_to_epoch("not a time") is None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import statbtrfs
|
|
2
|
+
from fakes import SHOW_INCOMPLETE, SHOW_MULTI_DEVICE, SHOW_SINGLE
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_parse_show_single_device():
|
|
6
|
+
uuid, fs_used, device_path = statbtrfs.parse_show_info(SHOW_SINGLE)
|
|
7
|
+
assert uuid == "11111111-2222-3333-4444-555555555555"
|
|
8
|
+
assert fs_used == "12.34GiB"
|
|
9
|
+
assert device_path == "/dev/sda1"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_parse_show_multi_device_paths_joined():
|
|
13
|
+
uuid, fs_used, device_path = statbtrfs.parse_show_info(SHOW_MULTI_DEVICE)
|
|
14
|
+
assert uuid == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
15
|
+
assert fs_used == "1.00GiB"
|
|
16
|
+
assert device_path == "/dev/nvme0n1, /dev/nvme1n1"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse_show_incomplete_returns_nones(capsys):
|
|
20
|
+
uuid, fs_used, device_path = statbtrfs.parse_show_info(SHOW_INCOMPLETE)
|
|
21
|
+
assert uuid == "11111111-2222-3333-4444-555555555555"
|
|
22
|
+
assert fs_used == "1.00GiB"
|
|
23
|
+
assert device_path is None
|
|
24
|
+
out = capsys.readouterr().out
|
|
25
|
+
assert "Could not parse btrfs show info" in out
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import statbtrfs
|
|
2
|
+
from fakes import STATS_MULTI_DEVICE, STATS_NONZERO, STATS_ZERO
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_parse_device_stats_zero():
|
|
6
|
+
d = statbtrfs.parse_device_stats(STATS_ZERO)
|
|
7
|
+
assert d["write_io_errs"] == 0
|
|
8
|
+
assert d["read_io_errs"] == 0
|
|
9
|
+
assert d["flush_io_errs"] == 0
|
|
10
|
+
assert d["corruption_errs"] == 0
|
|
11
|
+
assert d["generation_errs"] == 0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_parse_device_stats_skips_malformed_and_reads_nonzero():
|
|
15
|
+
d = statbtrfs.parse_device_stats(STATS_NONZERO)
|
|
16
|
+
assert d["read_io_errs"] == 3
|
|
17
|
+
assert d["corruption_errs"] == 12
|
|
18
|
+
assert d["generation_errs"] == 1
|
|
19
|
+
assert "not-a-stat-line" not in d
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_parse_device_stats_sums_across_devices():
|
|
23
|
+
d = statbtrfs.parse_device_stats(STATS_MULTI_DEVICE)
|
|
24
|
+
assert d == {
|
|
25
|
+
"write_io_errs": 7,
|
|
26
|
+
"read_io_errs": 1,
|
|
27
|
+
"flush_io_errs": 2,
|
|
28
|
+
"corruption_errs": 42,
|
|
29
|
+
"generation_errs": 0,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_parse_device_stats_skips_non_numeric_values():
|
|
34
|
+
d = statbtrfs.parse_device_stats([
|
|
35
|
+
"[/dev/sda1].write_io_errs n/a",
|
|
36
|
+
"[/dev/sdb1].write_io_errs 5",
|
|
37
|
+
])
|
|
38
|
+
assert d == {"write_io_errs": 5}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_parse_device_stats_empty():
|
|
42
|
+
assert statbtrfs.parse_device_stats([]) == {}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import statbtrfs
|
|
4
|
+
from fakes import (
|
|
5
|
+
SCRUB_FINISHED_OK,
|
|
6
|
+
SCRUB_NEVER,
|
|
7
|
+
SCRUB_RUNNING,
|
|
8
|
+
SHOW_SINGLE,
|
|
9
|
+
STATS_ZERO,
|
|
10
|
+
CommandMap,
|
|
11
|
+
install_command_map,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_issue_scrub_command_yields_cumulative_and_starts(monkeypatch):
|
|
16
|
+
cm = CommandMap(default=[])
|
|
17
|
+
install_command_map(monkeypatch, cm, statbtrfs)
|
|
18
|
+
mounts = ["/mnt/a", "/mnt/b"]
|
|
19
|
+
gen = statbtrfs.issue_scrub_command(mounts)
|
|
20
|
+
assert next(gen) == ["/mnt/a"]
|
|
21
|
+
assert next(gen) == ["/mnt/a", "/mnt/b"]
|
|
22
|
+
starts = [c for c in cm.calls if c[0] == "cmd" and c[1][:3] == ["btrfs", "scrub", "start"]]
|
|
23
|
+
assert [c[1][3] for c in starts] == ["/mnt/a", "/mnt/b"]
|
|
24
|
+
assert all(c[2] is False for c in starts) # wait=False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_main_scrub_first_wave_respects_max_count(monkeypatch, capsys):
|
|
28
|
+
mounts = ["/mnt/a", "/mnt/b", "/data"]
|
|
29
|
+
# mount output order is not set order; provide all three lines
|
|
30
|
+
mount_lines = [
|
|
31
|
+
"/dev/sda1 on /mnt/a type btrfs (rw)",
|
|
32
|
+
"/dev/sdb1 on /mnt/b type btrfs (rw)",
|
|
33
|
+
"/dev/sdc1 on /data type btrfs (rw)",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# lockout=2 + interval=1 → ≥1 status poll with room under max before lockout elapses
|
|
37
|
+
lockout = 2
|
|
38
|
+
interval = 1
|
|
39
|
+
clock = {"now": 1_000_000.0}
|
|
40
|
+
mount_status = {} # only started mounts appear; unstarted → Never
|
|
41
|
+
started = []
|
|
42
|
+
start_clocks = []
|
|
43
|
+
sleep_snapshots = [] # (now, len(started)) taken at each sleep entry
|
|
44
|
+
state = {"status_in_round": 0, "round": 0}
|
|
45
|
+
|
|
46
|
+
def fake_time():
|
|
47
|
+
return clock["now"]
|
|
48
|
+
|
|
49
|
+
def fake_sleep(seconds):
|
|
50
|
+
sleep_snapshots.append((clock["now"], len(started)))
|
|
51
|
+
clock["now"] += seconds
|
|
52
|
+
|
|
53
|
+
def lookup(command, wait=True):
|
|
54
|
+
cmd = tuple(command)
|
|
55
|
+
if cmd[:3] == ("mount", "-t", "btrfs"):
|
|
56
|
+
return mount_lines
|
|
57
|
+
if cmd[:3] == ("btrfs", "scrub", "start"):
|
|
58
|
+
return []
|
|
59
|
+
if cmd[:3] == ("btrfs", "filesystem", "show"):
|
|
60
|
+
return list(SHOW_SINGLE)
|
|
61
|
+
if cmd[:3] == ("btrfs", "device", "stats"):
|
|
62
|
+
return list(STATS_ZERO)
|
|
63
|
+
if cmd[:3] == ("btrfs", "scrub", "status"):
|
|
64
|
+
mount = cmd[3]
|
|
65
|
+
# Start of each table round (one status call per mount)
|
|
66
|
+
if state["status_in_round"] == 0:
|
|
67
|
+
state["round"] += 1
|
|
68
|
+
if state["round"] == 1 and started:
|
|
69
|
+
# Free a slot under -m while one scrub still running (lockout must still block)
|
|
70
|
+
mount_status[started[0]] = "finished"
|
|
71
|
+
if len(started) == 3:
|
|
72
|
+
# After wave-2 start, finish everyone so the exit path can fire
|
|
73
|
+
for m in list(mount_status):
|
|
74
|
+
mount_status[m] = "finished"
|
|
75
|
+
state["status_in_round"] = (state["status_in_round"] + 1) % len(mounts)
|
|
76
|
+
|
|
77
|
+
status = mount_status.get(mount)
|
|
78
|
+
if status == "running":
|
|
79
|
+
return list(SCRUB_RUNNING)
|
|
80
|
+
if status == "finished":
|
|
81
|
+
return list(SCRUB_FINISHED_OK)
|
|
82
|
+
return list(SCRUB_NEVER)
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
def execute_command(command, wait=True):
|
|
86
|
+
if tuple(command)[:3] == ("btrfs", "scrub", "start"):
|
|
87
|
+
mount = command[3]
|
|
88
|
+
started.append(mount)
|
|
89
|
+
start_clocks.append(clock["now"])
|
|
90
|
+
mount_status[mount] = "running"
|
|
91
|
+
return lookup(command, wait=wait)
|
|
92
|
+
|
|
93
|
+
def execute_commands(commands, wait=True):
|
|
94
|
+
return [lookup(c, wait=wait) for c in commands]
|
|
95
|
+
|
|
96
|
+
monkeypatch.setattr(statbtrfs, "execute_command", execute_command)
|
|
97
|
+
monkeypatch.setattr(statbtrfs, "execute_commands", execute_commands)
|
|
98
|
+
monkeypatch.setattr(statbtrfs.time, "time", fake_time)
|
|
99
|
+
monkeypatch.setattr(statbtrfs.time, "sleep", fake_sleep)
|
|
100
|
+
monkeypatch.setattr(statbtrfs.multiCMD, "pretty_format_table", lambda rows, header=None: "TABLE")
|
|
101
|
+
monkeypatch.setattr(
|
|
102
|
+
statbtrfs.sys,
|
|
103
|
+
"argv",
|
|
104
|
+
[
|
|
105
|
+
"statbtrfs",
|
|
106
|
+
"-s",
|
|
107
|
+
"-m",
|
|
108
|
+
"2",
|
|
109
|
+
"--scrub_command_lockout",
|
|
110
|
+
str(lockout),
|
|
111
|
+
"-i",
|
|
112
|
+
str(interval),
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Force deterministic iteration order for the set of mounts by patching find_btrfs_mounts
|
|
117
|
+
monkeypatch.setattr(statbtrfs, "find_btrfs_mounts", lambda patterns: set(mounts))
|
|
118
|
+
# And make issue_scrub_command iterate a stable list:
|
|
119
|
+
real_issue = statbtrfs.issue_scrub_command
|
|
120
|
+
|
|
121
|
+
def stable_issue(ms):
|
|
122
|
+
return real_issue(sorted(ms))
|
|
123
|
+
|
|
124
|
+
monkeypatch.setattr(statbtrfs, "issue_scrub_command", stable_issue)
|
|
125
|
+
|
|
126
|
+
statbtrfs.main()
|
|
127
|
+
out = capsys.readouterr().out
|
|
128
|
+
assert "All Scrubs finished." in out
|
|
129
|
+
|
|
130
|
+
wave_start = start_clocks[0]
|
|
131
|
+
assert start_clocks[0] == start_clocks[1]
|
|
132
|
+
assert started[:2] == sorted(mounts)[:2]
|
|
133
|
+
assert started[2] == sorted(mounts)[2]
|
|
134
|
+
|
|
135
|
+
# First sleep is immediately after wave 1 — concurrency cap holds
|
|
136
|
+
assert sleep_snapshots[0] == (wave_start, 2)
|
|
137
|
+
|
|
138
|
+
# Mid-loop: room under max (one finished, one running) but lockout not elapsed → still 2 starts
|
|
139
|
+
pre_third_after_poll = [
|
|
140
|
+
(now, n) for now, n in sleep_snapshots[1:] if n == 2
|
|
141
|
+
]
|
|
142
|
+
assert pre_third_after_poll, "expected ≥1 poll sleep with room under -m before wave 2"
|
|
143
|
+
assert all(n == 2 for _, n in pre_third_after_poll)
|
|
144
|
+
assert all(now - wave_start <= lockout for now, _ in pre_third_after_poll)
|
|
145
|
+
|
|
146
|
+
# Third start only after lockout gate (not merely "eventually 3")
|
|
147
|
+
assert len(started) == 3
|
|
148
|
+
assert start_clocks[2] - wave_start > lockout
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_main_scrub_exclude_recent_never_starts_skipped(monkeypatch, capsys):
|
|
152
|
+
# /mnt/new finished recently -> skipped by -d 7
|
|
153
|
+
# /mnt/old never scrubbed -> kept and started
|
|
154
|
+
scrub_epoch = statbtrfs.parse_scrub_time_to_epoch("Thu May 2 09:56:55 2024")
|
|
155
|
+
clock = {"now": scrub_epoch + 24 * 3600}
|
|
156
|
+
|
|
157
|
+
monkeypatch.setattr(statbtrfs.time, "time", lambda: clock["now"])
|
|
158
|
+
|
|
159
|
+
def sleep_and_advance(s):
|
|
160
|
+
clock["now"] += max(s, 1)
|
|
161
|
+
|
|
162
|
+
monkeypatch.setattr(statbtrfs.time, "sleep", sleep_and_advance)
|
|
163
|
+
|
|
164
|
+
started = []
|
|
165
|
+
|
|
166
|
+
def execute_command(command, wait=True):
|
|
167
|
+
cmd = tuple(command)
|
|
168
|
+
if cmd[:3] == ("mount", "-t", "btrfs"):
|
|
169
|
+
return [
|
|
170
|
+
"/dev/sda1 on /mnt/old type btrfs (rw)",
|
|
171
|
+
"/dev/sdb1 on /mnt/new type btrfs (rw)",
|
|
172
|
+
]
|
|
173
|
+
if cmd[:3] == ("btrfs", "scrub", "status"):
|
|
174
|
+
mount = cmd[3]
|
|
175
|
+
if mount == "/mnt/new":
|
|
176
|
+
return list(SCRUB_FINISHED_OK)
|
|
177
|
+
return list(SCRUB_NEVER)
|
|
178
|
+
if cmd[:3] == ("btrfs", "scrub", "start"):
|
|
179
|
+
started.append(cmd[3])
|
|
180
|
+
return []
|
|
181
|
+
if cmd[:3] == ("btrfs", "filesystem", "show"):
|
|
182
|
+
return list(SHOW_SINGLE)
|
|
183
|
+
if cmd[:3] == ("btrfs", "device", "stats"):
|
|
184
|
+
return list(STATS_ZERO)
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
def execute_commands(commands, wait=True):
|
|
188
|
+
return [execute_command(c, wait=wait) for c in commands]
|
|
189
|
+
|
|
190
|
+
monkeypatch.setattr(statbtrfs, "execute_command", execute_command)
|
|
191
|
+
monkeypatch.setattr(statbtrfs, "execute_commands", execute_commands)
|
|
192
|
+
monkeypatch.setattr(statbtrfs.multiCMD, "pretty_format_table", lambda rows, header=None: "TABLE")
|
|
193
|
+
monkeypatch.setattr(
|
|
194
|
+
statbtrfs.sys,
|
|
195
|
+
"argv",
|
|
196
|
+
["statbtrfs", "-s", "-d", "7", "-m", "8", "--scrub_command_lockout", "1", "-i", "1"],
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Import SCRUB_NEVER at module top with the other fakes.
|
|
200
|
+
statbtrfs.main()
|
|
201
|
+
assert started == ["/mnt/old"]
|
|
202
|
+
assert "/mnt/new" not in started
|
|
203
|
+
assert "Skipping mounts scrubbed within the last 7d" in capsys.readouterr().out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|