PRMICOEYQ5HHHN6PP5SBSBYU6ZZWLCD3DSUPDYCS2DVJ4XLNRCDAC
ExUnit.start()
defmodule CustomCustomsTest do
use ExUnit.Case
doctest CustomCustoms
test "greets the world" do
assert CustomCustoms.hello() == :world
end
test "grouper" do
assert CustomCustoms.groups("a") == ["a"]
assert CustomCustoms.groups("ab\nc") == ["ab", "c"]
assert CustomCustoms.groups("a\nb\nc") == ["a", "b", "c"]
end
test "unioner" do
assert CustomCustoms.unioner(["a"]) == "a"
assert CustomCustoms.unioner(["ab", "a"]) == "a"
end
test "count group" do
assert CustomCustoms.count_group("a") == 1
assert CustomCustoms.count_group("ab") == 2
assert CustomCustoms.count_group("a\nb\nc") == 0
assert CustomCustoms.count_group("ab\nac") == 1
end
test "part 1" do
File.read!("input")
|> String.split("\n\n", trim: true)
|> Enum.map(&CustomCustoms.count_group/1)
|> Enum.sum()
|> IO.inspect(label: "part one")
end
end
%{
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"},
}
defmodule CustomCustoms.MixProject do
use Mix.Project
def project do
[
app: :custom_customs,
version: "0.1.0",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false}
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end
defmodule CustomCustoms do
@moduledoc """
Documentation for `CustomCustoms`.
"""
@doc """
Hello world.
## Examples
iex> CustomCustoms.hello()
:world
"""
def hello do
:world
end
@spec count_group(binary) :: non_neg_integer
def count_group(input) do
input
|> CustomCustoms.groups()
|> unioner()
|> String.length()
end
@spec groups(binary) :: [binary]
def groups(input) do
input |> String.split("\n", trim: true)
end
@spec unioner(maybe_improper_list) :: binary
def unioner(inputs) when is_list(inputs) do
inputs
|> Enum.map(&String.graphemes/1)
|> Enum.map(&MapSet.new/1)
|> Enum.reduce(&MapSet.intersection/2)
|> Enum.join()
end
end
w
s
q
s
klfrwivqhc
w
wgyze
anw
khfraeogtbdscw
rdofujgnm
ydfrgo
dgjoqmrf
yrfdgzpon
xmivszjcnqhda
efwvanjd
dvjuywna
egjwvndkboa
mraiyzpxngdl
ynzdmgkxwpaiolr
ur
rq
r
xbkdvzgwuterocn
zukrvtbneogdx
umykaztlrodgbxwve
odujhzrpsxekgbiv
vhkmesczrwpj
cvkmrzhsewpj
rpt
qwu
j
k
p
crjqtmzkba
kjuhynv
xjnpivhszgofwum
cipqmtugyobxvwfhn
qgcmjr
rbmgjoq
zuekypfwdng
amsxg
ykwj
yjunwk
jkwyd
iwkzjy
qsdczjkoixlrnyvbmgtf
woickyhaguesfrnpq
pma
ma
g
n
rh
n
tjhcxba
bxthacj
abhtxjc
hjbaxtc
jbctxha
obuairedl
wauixvbogejdpln
ldciokbaue
myblsdeuiazo
tdembualfizo
bpumtwsxe
tsaumxw
hxuftswm
vqglbnuhej
xbglpwzyam
uelbgovc
qcdwjtghispmorvlu
mziopsvhdwtlgjuqrc
rdlspoujmtwgihvcyq
nojtaidspuhvmkblwercqg
dpqohtufrcvlmjgswi
jgmbovu
akiuxtg
rsuypgkdiofnbwt
npcdvyokbirwgt
mnybkgeortadpw
idt
idta
idt
idt
nidtry
amoxgqywpznistrlf
zqfidnsbyjgaxwo
nsyqijwxvohazfg
sioxnqfwazgy
ryhgae
ahygre
waopckqgeyrsuh
qrkwaegysocuph
csohuawqegrpky
ykqahecrsoupwg
eswjgdxfmkvq
jmtkwbvxqfedrs
eqkmozylat
yjmaezkrl
zbmkalye
zmaekigyl
ylakemzjc
tqlrzgwdokxnu
lkdanfmci
nhmejkdflap
mxviraylsouf
vbspyxfaewiruqktdczl
yhlravxgfisu
nxyvlusafir
rsvafxliyu
qlrgcjdpnyzofkxmeiv
kmazvnxscotrjeygdilpf
zxdnogtmlqjevfkiypc
pglconixjvbzkfymue
zyxucs
sxcy
csxy
xscy
xscy
uavrcwslpdqghmenbioxjyz
kjcqgrpdnymelxuzothawvib
lhbcumsfqrvzwnxjydpoagie
qgozpicjumxwaybnrlvdhe
vwdegcnoqafb
avbwqectgnof
acqefnwvgbo
oeqabfwrcjlgnv
ancqwovegfb
qva
va
xba
a
nhxeygm
oaypjdznr
ngzm
uhql
nyujw
uynw
ercqboln
s
yh
d
qs
n
efwcn
w
yzowkdciuqljeg
goiyjwzckludeq
yuekgwjodlzicq
qxkhclsg
xlqckg
qxkcgl
nwrxtshalk
fqoc
ypqug
pmfvzc
vzigd
diqentabxumf
utnmieadqb
udtemqinba
mbqidntaeu
imjpuxdlsr
pmrlxsjdi
xjpisrdml
ialpxshdmyjr
smjxzplrdi
eivhrugycstznxjkawqo
rhxwkizjeqayougtvncs
zbsygjuwacioxpnktqvhre
cyirhogexjzkwsntuavq
csrovwjqxnhzayktegui
h
fd
h
uphjsebqgklcomtwify
cqkltbwofhspugimeyj
wvrekmaxicufgyjbhqopsltz
qtobyikeschjmgflwup
sceghujwtfkpblyqmio
a
a
a
a
a
gskndyzmophwclqv
mylhsqdcnozpkgwv
snyodvwplgmkzchq
nyfjkgwielzq
shoumqbpxg
zlvkwrgaq
omndevzcwyajltfksq
ztfvpyjwdrlbmqoce
viqrlshezknaow
vheawroznstkblq
hxfswlvegjnoyrkqc
iblhwqneouvkrms
hdpsnwqrvekol
r
tr
srk
ryz
aqzkthji
qkwzjta
jzktqa
tgzkasrojq
nzikhjaqut
wr
wrg
wr
lmvpujx
bczhsgfaqt
fhadqpy
qbk
ixstvjlcrmwognuez
cpuznxavj
pxvuzcja
cjpuzaxv
upzjvaxc
hftodjcxvbrisuy
bhuodryvsfjxi
wtdf
nwuikz
lxmbjawysh
kdrtw
qfkw
ymxzrfh
zrwjdhf
lniw
wnli
wnzrli
ubwjo
sjzo
rjo
zikumqlvjobw
ktoaufevgrb
rnkbtovduh
jmvbntqilzhrgp
qhgzvkmtripw
psrqwgihzvtm
vmgqrtpihzd
vqrwipgmthsz
hnyzvkwgeo
ezhvoynk
evnhkoqwzy
hvznelgkoy
oyekhaxviznu
pwqivtzgebnolk
inqwtvgzopb
qwinobvgzpt
vbiowqtpnzg
ulsvitcxngwzhryaebkfp
sxypjlzgoihkrnubva
bzgt
gotxzb
ztgb
tbglz
lsrqikouybjdvawz
qnchepfoi
ufwavmp
vfpamwu
wfvmpau
vwumfpa
pmtfuawv
g
gh
e
n
swfjhadxgnuzrybokl
sleobifpuqhkzc
pcwfrnhsvdgejimlzoaubk
vducpzwnsjmhoilgkbaerf
rpbjnwvguahecofmkisdzl
ufbxshgloznvrwipjcqeadmk
hnmscakpleigdfjozvburw
ouraitbgl
trdmgi
fcxpstnyjg
gouat
nmrvxhdc
chnrmvxd
xvrhcnmd
mrcdvxhn
mxvndhcr
cufsjilngrbydkhqex
kljdhfaxyguqisbrw
dkgubshipmxrlqftoyj
jsi
sij
sji
isj
jis
prnxkmutl
pdvuaeyxochzltnkmr
gmxlutkpnrb
lxpmkrtnu
amcxhzlrpg
kpmowcr
n
ryn
an
n
qfgeuyn
unqcfe
haj
ocja
tbfmzce
tfmzbe
smzefbt
mbztfe
mfztbe
cgrtbynxejvqmhalf
hyafxecjqvrnmbltg
qtghbfrmcxaevjlny
fyqjgcemhlabnrtxv
h
h
h
wh
hg
mpidnesyhcowqtgulfk
oehmlsxqundfgbkytwa
hsqywtmpgazudj
tpuewnhoagzsydjlm
gdupwstzjahym
qwrpgikdyuatzhjms
mtaduhswzjpgy
yfcirgulotspbxehdzmwnkqv
aqvgyhtrniemlxpbcudwsf
bguysvaqwcmdjk
xmgfuybtkprqacd
rjmoslapxkvuht
uavxpsflhjoktr
hpcjtarlsqwkoxuv
jvakpxloushtr
iwcnso
bgwc
gpwcu
jwdiuyabzklmqsxv
uxyjkvqwdszflabmi
aiqwuszlxbkjydvm
vysjdmqlzwxaibku
mksabwzxqivujdly
a
alo
a
a
a
dzushfvegcopkbw
oueshcbkvdpwg
esbcowkgvpduh
pghduksijcevywobl
puezsvdohbwckg
ymow
moy
ohmyle
ymo
moy
mzctqbypdkrialjgoxsf
kqpoyabcigszdrtmfljx
xkfgltdipzjqysacmbro
lirmkbxsafcgdozytjpq
yhtvcsfaigk
wrevhoqdm
zphujdrlnv
zyahfewvcs
sfzcweayh
woeshuacyf
p
ljbr
wxp
cfdltixjupqon
nmuptcqodlfij
rpltfunoqjicd
nlqctporjifud
ufipcdtlnojq
xu
uowa
ux
ycloxsvrwajqbzfkhpgmun
wjlrmuoaxvkqfbpzsyhcng
aqrcybjgonhkpuvmzlfxsw
warfnczvokmyjgsqxhblpu
tvzngwyqbfulxcphoskjrma
yfhzaw
hmuazqwbj
ayhzwi
awxetzh
gawzih
gsbqoynfmevkcadjzu
dmyofukqezgasctvn
zqkmoadfnubsiveygc
gezqymskdfnouvaci
kzgncourmyfqeavsd
pzkyu
pyedw
ypr
tyifxjnsp
rbkqywup
mlwtuqxsog
xzmusgwtlq
qmitxvulsgwcak
uxwgjmrtslq
bcnkpwtmqgorsfxy
hmogknxfpqwsctbr
gqsracvfiobzkxnpmw
kpxv
pfkzxv
xpk
pkx
xubokps
bwlytxgviueczfqakrpsmhojd
fowlhcmvasbqdgptjikyxruze
nowi
ivonqx
if
lysuabtiprdkzh
u
u
u
dkrhoamveqlgcjwfsz
dgokizyvfjhaqwcme
thwguqxoadznekvcjpmbf
hyrsn
rhdyjsf
ycbzrs
smrykotpu
tsnfogyic
ingctoysf
cvnoazqpkthxwj
qhsmjdlznvkwfbt
qnvkhjwlstz
ltjzkugbqhwvnis
q
ytx
vxjyeorpcgbsta
oajbxtysv
tymsxjoabv
jwko
wojk
wjko
nfjvu
nfkv
dvnfse
toe
toe
ote
zqrmchysuf
oyzufmqslke
sgbwp
pgwbs
bl
klp
led
l
lq
ea
ea
rfeah
ae
aex
kbpnxtliue
kxbtieunp
exbtipuknr
nburkipxtel
igtpwhnejqbuckx
hfarpblqs
baophlqrexfsd
hbqfsnlpar
fspkvb
hfpkvsb
vpsfkb
fbnx
fknl
nftl
cuzownfq
fyn
qd
qfdzy
dq
jhwgyisz
vfkcdjqhme
hxwjo
jyhbr
hsuj
bip
jolwnbcgp
reptb
pbz
bpe
hrdiepna
vitecpzyndar
esbkaudrijpnlog
pxivdaenrh
hpfwjy
fjya
ajyf
yfj
r
rz
r
r
r
r
r
r
sqlcpxhtouyzwv
tyxhrsbuczklidogvq
aisdgc
mcdsiga
asigdc
dcisag
iswu
siw
sqwi
aixlbtqznjcgshu
oayljitqguzbchxns
suxgbcntaliqjzh
fubhpe
ufibkay
lena
izaj
axbvk
chav
fkoshgqju
jiq
qjwbi
jiq
jyqbi
ebaqscfvhgtuoirnjwxlzykp
hfjxqtuvrwgeknboipazyscl
qrdo
qlfejdru
pqxyra
gmnafo
fsgmepna
mfonag
pnxtgjqsbizkyc
ithkycsjbpzxru
snfwtloepqdrkiy
qkobliwsdnh
wsxjqnihzkodl
ahdwsilnkqbo
duowiqcmksnl
cjft
haorelpitd
gbxhedt
knzthvyaqu
vgyoklbrhedtwcjxans
eaxgovbrudfnhzktjsyclw
ytcgjxvodwhaenlsbrk
fnmhxj
lfnxr
xmnefc
wduktfvqgn
omf
jwfou
cpdgxqevabkrzny
lwhts
vhn
hirkuj
hzngm
minhkslfp
rfahobpksn
plfknsch
psifmcnhk
xkoiztsqvpceywhfbndu
ebkpvsunzcxhmofdiqyt
nfp
fne
fn
hinmuatrvygoqxfsjc
vrfxtuagojyiqnhscm
tifguysjoxrnqmadhcv
yetihjzfkndum
nejhiatzumkyd
zitvhkdenmjyu
hinjeumtzykd
ekztmhndjiuy
glbtefawdsycomj
djbsgnalyokcwmte
mojdwysnategclb
bvgcjosaytwldemp
evwuatlypjixqzrsmhc
erwiucsaxmtvhyjzl
lhcxeavjmyzurtsonwip
mxveyutrhslgbaczdjifw
snmyhxctuazerkwlvji
rvpwjbh
yfecq
vxbchwiygoe
gwivcydbxeho
ylogwcevibhtxa
bgwphicvexyo
emivwogychrbx
pwghmnxtfzbyl
vqsakujrdo
areduqjvczsm
jadhzebrcum
dcrumqayjzei
rczsujymdea
omtyndvrha
vwdhaort
crb
r
uirepncmbyhzkaofwxvd
recwkxyhfnbmzaduovs
kwceyznjbfoidhamrxvu
lejmaipctkvuwgzqfsnor
okmuaizfsrtnwlevjcgqp
lqkwnraeuovstmizpjcgf
atsunvqljzepckrmogwif
rjobteshildmxacpz
lrpdbajtmhcsoexiz
l
pvnsiom
khgx
f
f
khjgfwy
fpgykjw
wajntv
wdzuvtj
iowjvtke
fwvopmi
singjtf
nocq
nugbvmqeo
qaoscn
rlkzoi
opfsta
qvuo
soxp
xhmoj
guvqownkfrilte
aegnhfyxcszqbwvl
qzo
qlr
vquo
fgq
nkwhbdactmxijq
ijqgmnkeoxwuvbta
tiubnkgwamoqvj
ajdkuqtimbpvonhgw
mjgnyqitvwraobuk
s
g
g
g
vspbciqmaj
pijumqscva
evpqzoxgtmbns
tevgqbpfsnxl
epnbtgsxvq
qxpsnetgmvb
depavkqngtrbjxs
aplrhkuiyf
mxhpvwfzolsn
fxtbhediuslacmy
hbkufmistx
ihupfsmtbxz
mfhipusxtb
xihmtjbsuf
muzqcfp
mcpfzu
mrzfpucw
zfcvmup
pzcufm
q
q
q
q
jbk
peiw
vuejaotbcp
ulbakiotxdcry
cobunat
bmcfvhauto
qpo
qopw
pqo
ckpequzlfdtrb
yphsdgwbzn
bgmpzd
nbuqtywiloaesz
rkghxdvf
owetrs
etbwciulf
heaudnstmjwrpg
pxkyniszuebmolhfjdgr
jenrsqcupdwhmg
pujhrdmcengs
cutnl
ltunc
cntlu
hd
bih
rdf
drf
kftwxpoqmgbuil
wfstpikuhqbcmxog
zktqfjpxayobvumin
qgtfbopkruidmx
sovyuxi
vux
uvqx
j
j
j
j
mav
qark
tdeaquk
zaxnwcpylkgvoqmhi
kaq
rfysxdtumlwavingkhqo
gbudftnxypmzrlevakjoq
qafyltcembujhd
jmaqtdublyfche
dmhbqljfeaytuc
emdtqyjahucbfl
tcbfjlyemaqudh
cpxhbjlqwarm
bhlzcawpjmqyg
cjlwbamhqpe
fhlbjcmpqawv
jalmbqiphwc
zoyw
yzo
qvudhfxtjesibgwynaorl
nuctmlvpkgoefqsihbrwyzxa
mgkqthideusny
ohtgleqksudiynm
nhgikyustdqme
puqgrdyntmhkise
hlmfcir
lcmfrih
rlkcyfmhin
imhcrlf
ifqds
spodiqf
ifdsq
ajp
apj
dramqyjeiskugn
qidmekaysrgpjfun
egysqnkrijluadm
ngdirjyksameqlu
tmpogzdsxrye
sryedpmqxzg
zcxpmlsukydreg
dpeolsyxkwgtzijrm
mefyrxdpbsvghz
qafgj
agfjd
hrczjfebg
fjergzc
cfmrztsygvl
istujvdk
keyoahi
ktwuhfqas
qksmthdwaru
ajkthusqpw
eprjtfahignbcklvuqwyx
apxqrceguwtjnkbhyfilv
drosxgbv
orvbgx
xgrobv
ylmpr
yrmlp
auwizmytjcsdbnvo
ywsmkoatibzndcju
nytqecdbxhlzmiwuoarsgj
vtdowcfjymsauknbiz
z
z
z
z
hxjiqdaveoku
jxqkdvheioau
hxoqdueajvki
tuxfs
usxtf
uxtfs
dnaiovgxlryzhqwepbmuj
zbgxvpaudnmwrqohejyil
qaybxdnelzokvjhmwrgusiftp
pyoukjfretqzsdxmhwvna
szwcvfkohpuyeqntrdamxj
jzkrfqvyptahxsmnowdue
yepoudnfxzkwjmqvtrsha
pd
e
m
lq
xcyvzopaubf
zhuyovbpcx
bcxuyvgzhop
dyczxoubvpmsw
zovykxucpb
w
w
w
h
aohk
kao
vkyhoa
oqvak
akjow
r
r
g
r
clnhiwztgq
ydfxzrcvjg
otmchepjl
gkvunyfwlm
ruzxit
ixtzdr
xrzit
xqzitckr
trizux
grufeslzwvxqndhimakcyjp
soaxtzmwgjdlnkpcqevrfuiy
bdaxjepnmzkruslyvqifwcg
qjiysmvhxfurkaegzdwlpcn
cerjstxhanlzkqgodfuy
crjsonqyuvdaelhftxzgk
rksaucfxdgmoqnhytejzl
gsnuojthickrealfzdyqx
wmstjfqhvaplord
fgnejqhkouizpvt
ojzucmk
upvmaozj
uhimrxjoqgzbw
utzjoemk
vztojmeu
elqmorhugsxypbtza
ogtlyehzpfaqxurbms
eqbgrxhauymptlzso
xldewg
lgdxznew
zexwgld
ldwxge
bghlxviwed
pgqovbkfxciz
fqobpzxkct
czqfxpbko
h
h
w
h
h
gw
wg
wg
gw
gew
byvadoriwmkhfqtcznglusxpje
xmwvnjpglyeocqhfarusbkzdti
zlrgmsbhinxvqueojtfawdcpky
pwghxcjybruidskelqmvaonftz
zwqlxogbvtuafeh
xrhaelugfwkoztb
zytuelfhoawxgb
deglztxbouawhf
xuageoztfhlbw
rqydeszwhu
uwzydhqcers
yqsdhruewz
reyhqdzsuwn
bomazjw
wzabmjno
jamzowb
ojawbzm
wbhzfamjo
pxzofjkney
aedkui
iezhxcldqrasu
givchplftdorun
xy
gydxv
igduyfakwjeot
utewfoajykid
cvudfonrqshzbgyemtkxlap
fxlapvmdcqzybugnhrketso
fkbersnvythqodmuaglzxpc
luqerdogftkmxnyzvsbahcp
fsolzkxvdtughqmabcyrpne
zwayveu
dvpwyiztgae
yzeuawv
evarwyz
tpkeivrucdhsawmn
imspcednuvw
epmnvlicsdwu
icmupenvdws
fmswudvpeionc
nbekcurq
rnckqbeu
qrlkcnbuv
buerqkcn
jylehvp
ejvlyph
ypjvhegl
ehpylvj
hlvyjfep
cnwjxzisphtdgveqky
njqvwdyhitocxzkpesg
teywxgihcsvdkjpzqn
hdiqyntzgxwejpsvkc
a
a
ma
cd
cj
nzekclihqfyrovdwgtusm
eosqnkxdpcuftzhwjmrily
ynimldrewskufczohqt
foreinluhctqkyszmdw
gxwfcasjqbnoyrv
cnqjyfvwgas
vfqangjcwuys
en
jn
n
gnzdf
knarqm
lknhmars
wpenjuhd
krucdjzb
nutjhd
xiejmt
cijktnpxme
pmixcj
xoijm
bzjqrmighx
ftucvebloxijamnzwy
vqxtyiucdlnfbzaj
cntxajbfslviukzy
cbtxlzjfsudvinay
dvftjyizuxnbacl
ty
ry
y
kanrgxtibzsjydf
pscfavzqyb
blasyzfcvum
wfhzasbey
ampozwyfbuqs
ihmpuwjfvygbnze
vzgcwefnihymxpubs
hvmbuwznygpife
vzdiknatrgmhyebwfoulp
hretmjgxwcidl
hjkiedvmcglx
jdmilhxewgc
clhgbjndxie
gnabr
rnbga
bywnuar
lhd
d
mqdbo
ahd
d
kisuyql
qsymkglu
skvuiyplq
xhyabuesjpd
lxagr
xzlaqt
kepiznwbyas
gyfirmnbodztsv
b
b
b
b
zskcmdxeaogqbu
zmbscodakqupxge
e
j
b
q
blvwcsymj
vwjsymbcl
wbmlcyjsv
mwjvcybsl
symvcblwj
yivbad
dayibv
bviayd
idvyba
ivadby
atzulnbxedjgq
egbvijuzyxtqadl
ladebjzrtgu
blzdjetaug
ehpzmcdgtuasbkolj
vejskrgaqnbltxiy
bviglyatsrno
iwcbrlysgvntad
gsliobcrynavtp
ybowxj
sufavcpghd
fjcikx
cjxki
cjxmik
xjkic
cjxki
o
tz
p
o
p
f
h
f
f
f
n
n
din
udlnyspztvcf
uzpdhtcayv
ydhzcvutp
teylnzmf
lntmefzy
lnztyemf
ugwcfqzrpsdxhnyv
ldfosniyuwxhcvbg
lxvdumygwfsnch
vcowxgynhusfd
qwxuykodtpehbzmgsrc
nyxqougapktdbh
ykdpbxtfquohg
tubxpdygoqhk
zgyomlb
moglkbzy
yogbclmz
qrejtivfwzubyknaplhmgcodxs
witcplundkhaqxeryvbzsgfojm
fvoindcxqhzabketlug
xgdqizetlhvkubaofcn
xciztehdlufnkvboagq
hbolxizvkmctw
udoarnhsjwk
owdrhsk
ghfwkyoea
cfzkvieqtoagsln
egtolisqrcf
stlqcifgoe
erjlptikwqoczdgubanxvf
evaqfnxljitbkdwucozrpg
ojqbicpwexfglrvyastzk
pzvisdhlortfqjxkwgacyeb
xiezsdhj
hwvgtplfs
hioskr
atyqbkzlw
bzakwqylt
ovwpzaybqktjl
ylakwtbqz
flxozwatvkcmibjn
wcxhokusjzvmbtn
jm
n
vmtwznjklocridexufapb
nztijrefwxuvkmacdplb
wjtipvfzenlmcrxuadbk
vpyefticjsxrzdauwnkbml
sjryquvzi
qvzsiuryj
vyjruqsiz
quykrjszvi
iqgpeslwatumyvxrkndhcjb
gcdqpktlmuzwjrvbsinha
usbcltqjmvpagndkrwhi
hkatgblrqpidnwjumvsc
ijrwnhlfsaxuy
cyzdt
cyvpm
qpkybtoe
agtpnyvweqrmohfil
lmtwhfgyzneaipq
yianftmwsgqlpeh
ezfsihltawgpnbmqy
fvdj
djv
jybuhgeapvnk
qdmscixzlto
wpvgfar
dtlbwgjyxphmqus
hkjxdtvqngyzar
i
i
v
jpbamlvyowxsgkzinc
dwijheuxmkbstqpnvoza
pnfbzdmysukvlj
fzlksnydpjumvb
djfmbqnvyzskplu
ylxfuzsdr
yxvfrzlkus
yxlsfwrzu
yulsfxzr
yrzuxlfds
cq
qcp
g
hyadprtkis
tykpiharsd
sdyahrtpki
pakifrhdyst
ritysadhkp
tdnuxmkhcveqgwasyr
vmhctngxuawsrqykbed
x
dhkts
pfg
fg
oz
qnudf
owzeg
axkvulniypoezgdmcht
kgeponhxactvylzdmi
fdkotcgiwhnljbqmepazyrvx
udhtkyexzncopglmvia
olhadkzxiepngvytcm
cxilwnkmdatosv
saovmlncxiktwd
swdmlnaivkotcx
kdclnvtsiaoxwm
vmndksiawtxolc
pla
ifgjtx
zvfsaiykgrh
vgzafikhsyr
faghrvizsyk
bvsldjymgzcqokie
zstldyexgrwokvimjqb
oqjgalksbymizcedv
thkil
puas
vqhlt
gvbhrqewyxj
bgejxvh
jsvibeagx
geftjxbvhw
exvgjbwh
ywjaichxrfvq
lnugcesm
tcmoe
etkbc
vlitomya
vmiplyoa
amvliyo
ayvlmio
ovfdmajyli
pxeqymbdt
fetymxnzs
ogerunlazpijskqfbmtdywv
orgudspwmfzvyljniakqeb
jkruynsdbcxifelogmpqvwza
auprhvlodbwmqikgsjyezfn
kjmehoatdxslfgrwyni
dzrnowjumiveqaltpfkc
jwmktnafrdieol
z
hu
pd
ap
pjx
hzjqrw
qrzjah
bvjizfyq
wxpgcbzie
ctegxwvh
egxcqtwf
zmwjpnviqde
npgijdvhzmwq
zwinjpqvmhd
tmwzjdpiqnv
thoezprianclgudvksyfbw
gizdnfqwvebathopyurkl
qeipwgrbvfndjtazhlokyu
vodgpzryhxwtlenkfuiab
tufkbyhegznadjilwvopr
dbvigopmznrclaexjfwkyhtq
kbvgmrioznjfycapxwleqdh
zrqgpeasymbhokunvjxdilwcf
ahmsunwpjzrtdlxqey
mjuqrshdatxnpzelyw
nrjqamdstpxlehwzuy
qyxbahsrlmvzwuedjntp
hudayszfmxptwljrqne
zdanj
manqwjed
anzd
jqdnwa
fcaxdbn
rabxt
brta
arbt
tarb
ajoshwczdmxkrivp
fxpskmtchuzra
pmyhacuzrxgkts
bskzphxmcrael
x
x
moay
omay
izocxhdkenqyavb
ihlpsrnkfo
twnpikho
ftpahurnomkcvdzily
ckapvofuzdlybtnrm
neglmcpyrdzjavfutowk
cdlnuqbvfejao
vdsocqlfebxnau
enjduqfcavblogm
yfnhbem
d
j
izhy
ihyz
zhyi
ijzhy
yhzi
jxzchrsb
xhi
vegaxdn
sfjopyx
xu
yxe
x
x
x
nyhlqrtzipagkmowuefvbcd
ewhoialgqyfpmcnrdkzj
roqhlinpdfwycmkgzase
ytwerh
utgwoirne
wxsm
xnjpohdmgykbtsraiqfu
sjphqtbuoankirgxdymf
amqpdilhfsytgoxbujknr
fsxordjuayhgkpiqbmnt
ipsvgmzyaqfdrtkuhonjbx
xwekvftlbnujarm
jweukmvanrbxltf
luebtmwjranfkv
jfuvklrbnmtwae
rlhbvemkjfanwut
pidycjeznrvh
vcjbepznry
wgnjvyofcaiplzer
ermiyzpvjcnh
pxrjqetknzvsyc
qnxmivda
xvdqim
qvxdmi
vxmqid
vxdmqi
wfapsg
scwgdufr
osfwg
zfayobrc
fyzobarc
zobcryaf
rfboycza
musbrjnygkftpoxad
sfnjgmoutkxrbyac
nljabtukighmrqyxosf
hwqxn
wnhxq
xbqhw
vg
qv
v
v
qv
lwo
vwa
wus
iwecv
dzvau
ynewjdhtlcm
npydwjmehlc
l
l
l
l
l
f
m
z
gqbawtycdnzhfjvix
wgnidzfxtajqychvb
dfqzjwvanycxtighb
ndgfatjwixvqhyczb
lvsftm
mtvlsf
ltvsfm
fvltsm
tlsmvf
wdvhntmbcausfgyk
mcanyusbkvgftdw
mauygksiwvftbcdn
cftwojgesayvkmudbn
kvdbcwafyzmnugsqt
a
bhngw
a
vu
prxqlit
pitqzr
featyrpmsi
iropt
wkux
txg
dto
o
bj
uagbxqkwit
wvthxnqgaki
hjqikngxaw
odarkwgqpi
wqagki
atobwlprngejvmixdqcu
ofwbtepxrqgimuyhdajkl
hqeotif
mpnvuyas
qzdlxrp
bdxzqrpl
rhqzpldx
qxpzrlbd
wp
pg
dyvph
wp
pn
netpfbl
jario
w
pcefadnkhzv
kgdhaupicxnezf
eadohrcjnkgiypsmtlfvzuq
hzaimfoqcuxtpdveykrgj
aytcjiumfpkdvgrehozq
btquhdlgfrmejkoiyvpzac
qcvkdrywezfmtiujaoghp
msueprdbjnzflgvytcwo
glftyscmvbojzudwrpen
jlgyfsntvwmrzdcuopbe
osivwyfumczldjbgretqphn
tuikjg
nmibokj
hbjikl
bzgymjnd
ndbygmzj
yjnhbdmz
jnyzbdm
gibndyztmj
iorhgcxbfjamwyenpvdksqu
evytoqjxznkbadhrmisgfcw
wbengjckvhmasfiyqrodx
esdcnwxjohmqkrgiybfav
ohkxbjgyvdeiqnsrcmawf
mkqgwxicp
qvailgsnxymo
xqrmidg
gqxihm
xqmigrp
zecksafgl
jlazcgfek
ugclqeakzxrf
cgkoflzae
eawcgkfzl
lehtfqcx
wojlmpdu
bnsjyrcmah
mynajrshbc
nbrhscamyj
jymhabnrsc
cybjsrnamh
uzybinv
zvblpy
xvlmybz
aeuyibogwkxpvjt
fismchnrzdqwp
hgsnrqoupzitwafvxek
hurvxiqezgknofaptws
wagytrequfixhpvzonsk
opknhiteqjwzxlfauvgrs
rghduv
aijsybl
fboy
body
azhcqrk
kmqfcuaz
mickatzqhg
qaosvkdzpcb
cazkgwhtq
snpgwemqclyd
gfjhe
rvgeubx
jerkbigo
oeigax
lhsifzcg
izhcfgls
ishgzlcf
msfqhtecuibyg
gdwkvyiq
yroxlivgqp
wftdvlgcubj
tsafjbwuiyxlqmr
nsvibpqze
y
murd
cbtukihfeqjrnv
uchikvqtebr
jgszxqvyfrakpl
aslgzyfxpjvkrq
sgyjaxzqvlfkpr
yprgqvkxjsflza
blxpv
xvlqsp
lvbxp
bwu
uwb
kuctazm
atcuzk
tuqkczad
cupkzta
iupzckat
virjwanfcxdsqkyzthmub
shtdubncmkayzfqwjxri
sdrvpcqybwomzah
mvhskw
swhemlv
wmvsh
hsmwlv
wrivxopdhgfauslbe
vnfeoxdwrmiqlyapcbsgu
dolivuprewxazsbgf
zudaxgosplbvfiwer
oxiglrthpasvfwudeb
gcjihxvtazydernl
exdhjtgacvryu
xqjhgystbrdaepcv
dcfryegvmjaxthz
n
n
yxna
un
uvsezitjwl
midztxjcesgrw
yulwizjset
cpjn
cjn
ugnj
njic
jwn
kmzjoe
qhsvdg
ta
dtzar
rtdz
btzrd
duzrt
nkmszydrtxv
rpjv
oqvkyjp
vemjp
pjvm
jtvp
jqbywgpexdrc
eihgzxtrknsl
zukegspqxhridyf
usfwpaihyjkxzerdq
zyfriudebqxpshk
nzsiehmplbjgfqw
egwziblfhsqmjpn
zslbnghpjfqiewm
wgniseplhzfqbjm
gaznqpwsembihlfj
d
knijh
d
xed
lev
cfzpjatlw
taclzpjwf
cltjfzpaw
fptzjclaw
tfcpwlzsaj
ni
ind
jnpydc
dynjpc
jpcynd
ncdjyp
wuilxjabeftcro
shpkudnygvmrq
yqzhalsjf
azutsmkq
qaszr
qmrzsxa
wjv
wxjv
wjv
wjv
esgpbow
epogwbs
oewpsbg
wpgqmscobe
ewsobpg
b
b
bw
rgz
jvrgpa
tzrgk
rtegf
rxgk
bcotpdrwmu
dwkmpbtcur
rdzyjbvsmuoacwkg
fhciwsnzamkugpdybrvox
mwpgv
zvw
wbyktilsjuemvqpdornczhx
kyfzbqitgahrvsuocjpledmn
qhytnurliebszcdvmpojk
ohxsjvdzqabegp
oqvgxrjbtelpuw
gqevkbpjnxoi
fcjsuviaq
asvcfjuiq
aqcfsujiv
sicfaqjvu
vaiqusfcj
icjtklarqhsmdy
qcamftiujys
zibcntjyasgoxeqm
wvyitlamrqsujpc
hnmxwtudcsbp
ryzdijaguqvfko
twqousdgyknvrmx
fpijbzeh
ixg
mxiv
xi
w
wu
w
gw
e
e
nktazsgmqvp
kxtcvsaznmpg
xm
k
m
mx
ntfkyoarlejixbd
colnibtxeryjadfk
yagxfwbisvhurjmpkqt
jfqhmekxduwari
xhwfrdmqjuiake
lajdxeuikmhrqfw
ihdkmeafjqwurx
otjeuvbqshdncxa
pmlcvukgsexriyabhq
bnvxquaeodtwcshf
fgeu
oec
mjflpw
jlwfpim
fwmlqpj
jpfmwlk
vpjcbfaihnusglr
rbvpcsfyihnjazguxl
kq
k
k
pign
pkgh
ajp
pq
pzomcb
qbo
hstryu
fljioe
nypszgxrdhbe
rxeyhpnsdzgb
epghbynszrxd
azlbegicyv
iavnezslg
ymtkef
oasrun
wvzl
uerpw
qcidxbhgj
hxqgt
gthqx
pocvsz
otkwzpvs
zvosp
vopcnzsl
vzmpnfos
rz
zgr
hrvze
rsz
uebwmzk
purbtkdm
eyumskvjib
d
d
d
nd
kdarcnph
kouwr
tuexkor
hnslcrtwyudpbxgv
htsrvqelyagudwbpo
mtubsahrkvpeyqwdlgf
r
r
r
r
r
xptijvhfa
xpricabvtfhj
vjpaxfhti
ahtxfvjpi
vhftijpxa
bxshqfzgyr
bzgfxsrqyh
szrqhgfxby
txbczyhfqsrg
ybfhsqzxrg
taevdznpkf
ezadwvfnkplt
kdpnzafvte
tvfdknpeaz
ea
a
a
a
a
dzwyofjxnugvbipar
uwrfvygonibzadxjp
afcm
cmaf
facm
lcisgd
jbkcid
y
y
djgyen
yl
tfbvzwocks
bzfstkvjcoy
kzuvsobxficthn
kftzogvcbs
vszjkobtfygc
heszp
uhzepx
zehp
shepz
b
ymel
z
zhq
s
wvhlzgcet
bkuwzeopm
nbjwfaeqmkz
mqaufjvwl
qlfuvirnjw
yvuwfglxejzt
gkylzr
lwdiykg
zuloiwargvxk
evxcy
gjrvnwbxu
bvpugixnwj
uwrmjvngbx
gcnvwbxuj
jwnuxbvg
q
q
q
q
ewq
zwynrv
uvxym
#!/usr/bin/env bash
# Start the program in the background
exec "$@" &
pid1=$!
# Silence warnings from here on
exec >/dev/null 2>&1
# Read from stdin in the background and
# kill running program when stdin closes
exec 0<&0 $(
while read; do :; done
kill -KILL $pid1
) &
pid2=$!
# Clean up
wait $pid1
ret=$?
kill -KILL $pid2
exit $ret
defmodule MixTestWatch.Mixfile do
use Mix.Project
@version "1.0.2"
def project do
[
app: :mix_test_watch,
version: @version,
elixir: "~> 1.0",
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
deps: deps(),
name: "mix test.watch",
description: "Automatically run tests when files change",
package: [
maintainers: ["Louis Pilfold"],
licenses: ["MIT"],
links: %{"GitHub" => "https://github.com/lpil/mix-test.watch"},
files: ~w(LICENCE README.md CHANGELOG.md lib priv mix.exs)
]
]
end
def application do
[mod: {MixTestWatch, []}, applications: [:file_system]]
end
defp deps do
# File system event watcher
[
{:file_system, "~> 0.2.1 or ~> 0.3"},
# App env state test helper
{:temporary_env, "~> 2.0", only: :test},
# Documentation generator
{:ex_doc, ">= 0.12.0", only: :dev}
]
end
end
defmodule MixTestWatch do
@moduledoc """
Automatically run your Elixir project's tests each time you save a file.
Because TDD is awesome.
"""
use Application
alias MixTestWatch.Watcher
#
# Public interface
#
@spec run([String.t()]) :: no_return
def run(args \\ []) when is_list(args) do
Mix.env(:test)
put_config(args)
:ok = Application.ensure_started(:file_system)
:ok = Application.ensure_started(:mix_test_watch)
Watcher.run_tasks()
no_halt_unless_in_repl()
end
#
# Application callback
#
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
worker(Watcher, [])
]
opts = [strategy: :one_for_one, name: Sup.Supervisor]
Supervisor.start_link(children, opts)
end
#
# Internal functions
#
defp put_config(args) do
config = MixTestWatch.Config.new(args)
Application.put_env(:mix_test_watch, :__config__, config, persistent: true)
end
defp no_halt_unless_in_repl do
unless Code.ensure_loaded?(IEx) && IEx.started?() do
:timer.sleep(:infinity)
end
end
end
defmodule MixTestWatch.Watcher do
use GenServer
alias MixTestWatch, as: MTW
alias MixTestWatch.Config
require Logger
@moduledoc """
A server that runs tests whenever source files change.
"""
#
# Client API
#
def start_link do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def run_tasks do
GenServer.cast(__MODULE__, :run_tasks)
end
#
# Genserver callbacks
#
@spec init(String.t()) :: {:ok, %{args: String.t()}}
def init(_) do
opts = [dirs: [Path.absname("")], name: :mix_test_watcher]
case FileSystem.start_link(opts) do
{:ok, _} ->
FileSystem.subscribe(:mix_test_watcher)
{:ok, []}
other ->
Logger.warn """
Could not start the file system monitor.
"""
other
end
end
def handle_cast(:run_tasks, state) do
config = get_config()
MTW.Runner.run(config)
{:noreply, state}
end
def handle_info({:file_event, _, {path, _events}}, state) do
config = get_config()
path = to_string(path)
if MTW.Path.watching?(path, config) do
MTW.Runner.run(config)
MTW.MessageInbox.flush()
end
{:noreply, state}
end
#
# Internal functions
#
@spec get_config() :: %Config{}
defp get_config do
Application.get_env(:mix_test_watch, :__config__, %Config{})
end
end
defmodule MixTestWatch.Runner do
@moduledoc false
alias MixTestWatch.Config
#
# Behaviour specification
#
@callback run(Config.t()) :: :ok
#
# Public API
#
@doc """
Run tests using the runner from the config.
"""
def run(%Config{} = config) do
:ok = maybe_clear_terminal(config)
IO.puts("\nRunning tests...")
:ok = maybe_print_timestamp(config)
:ok = config.runner.run(config)
:ok
end
#
# Internal functions
#
defp maybe_clear_terminal(%{clear: false}), do: :ok
defp maybe_clear_terminal(%{clear: true}), do: :ok = IO.puts(IO.ANSI.clear() <> IO.ANSI.home())
defp maybe_print_timestamp(%{timestamp: false}), do: :ok
defp maybe_print_timestamp(%{timestamp: true}) do
:ok =
DateTime.utc_now()
|> DateTime.to_string()
|> IO.puts()
end
end
defmodule MixTestWatch.PortRunner do
@moduledoc """
Run the tasks in a new OS process via ports
"""
alias MixTestWatch.Config
@doc """
Run tests using the runner from the config.
"""
def run(%Config{} = config) do
command = build_tasks_cmds(config)
case :os.type() do
{:win32, _} ->
System.cmd("cmd", ["/C", "set MIX_ENV=test&& mix test"], into: IO.stream(:stdio, :line))
_ ->
Path.join(:code.priv_dir(:mix_test_watch), "zombie_killer")
|> System.cmd(["sh", "-c", command], into: IO.stream(:stdio, :line))
end
:ok
end
@doc """
Build a shell command that runs the desired mix task(s).
Colour is forced on- normally Elixir would not print ANSI colours while
running inside a port.
"""
def build_tasks_cmds(config = %Config{}) do
config.tasks
|> Enum.map(&task_command(&1, config))
|> Enum.join(" && ")
end
defp task_command(task, config) do
args = Enum.join(config.cli_args, " ")
ansi =
case Enum.member?(config.cli_args, "--no-start") do
true -> "run --no-start -e 'Application.put_env(:elixir, :ansi_enabled, true);'"
false -> "run -e 'Application.put_env(:elixir, :ansi_enabled, true);'"
end
[config.cli_executable, "do", ansi <> ",", task, args]
|> Enum.filter(& &1)
|> Enum.join(" ")
|> (fn command -> "MIX_ENV=test #{command}" end).()
|> String.trim()
end
end
defmodule MixTestWatch.Path do
@moduledoc """
Decides if we should refresh for a path.
"""
alias MixTestWatch.Config
@elixir_source_endings ~w(.erl .ex .exs .eex .leex .xrl .yrl .hrl)
@ignored_dirs ~w(deps/ _build/)
#
# Public API
#
@spec watching?(MixTestWatch.Config.t(), String.t()) :: boolean
def watching?(path, config \\ %Config{}) do
watched_directory?(path) and elixir_extension?(path, config.extra_extensions) and
not excluded?(config, path)
end
#
# Internal functions
#
@spec excluded?(MixTestWatch.Config.t(), String.t()) :: boolean
defp excluded?(config, path) do
config.exclude
|> Enum.map(fn pattern -> Regex.match?(pattern, path) end)
|> Enum.any?()
end
defp watched_directory?(path) do
not String.starts_with?(path, @ignored_dirs)
end
defp elixir_extension?(path, extra_extensions) do
String.ends_with?(path, @elixir_source_endings ++ extra_extensions)
end
end
defmodule MixTestWatch.MessageInbox do
@moduledoc """
Helpers for managing process messages.
"""
@spec flush :: :ok
@doc """
Clear the process inbox of all messages.
"""
def flush do
receive do
_ -> flush()
after
0 -> :ok
end
end
end
defmodule MixTestWatch.Config do
@moduledoc """
Responsible for gathering and packaging the configuration for the task.
"""
@default_runner MixTestWatch.PortRunner
@default_tasks ~w(test)
@default_clear false
@default_timestamp false
@default_exclude [~r/\.#/, ~r{priv/repo/migrations}]
@default_extra_extensions []
@default_cli_executable "mix"
defstruct tasks: @default_tasks,
clear: @default_clear,
timestamp: @default_timestamp,
runner: @default_runner,
exclude: @default_exclude,
extra_extensions: @default_extra_extensions,
cli_executable: @default_cli_executable,
cli_args: []
@spec new([String.t()]) :: %__MODULE__{}
@doc """
Create a new config struct, taking values from the ENV
"""
def new(cli_args \\ []) do
%__MODULE__{
tasks: get_tasks(),
clear: get_clear(),
timestamp: get_timestamp(),
runner: get_runner(),
exclude: get_excluded(),
cli_executable: get_cli_executable(),
cli_args: cli_args,
extra_extensions: get_extra_extensions()
}
end
defp get_runner do
Application.get_env(:mix_test_watch, :runner, @default_runner)
end
defp get_tasks do
Application.get_env(:mix_test_watch, :tasks, @default_tasks)
end
defp get_clear do
Application.get_env(:mix_test_watch, :clear, @default_clear)
end
defp get_timestamp do
Application.get_env(:mix_test_watch, :timestamp, @default_timestamp)
end
defp get_excluded do
Application.get_env(:mix_test_watch, :exclude, @default_exclude)
end
defp get_cli_executable do
Application.get_env(:mix_test_watch, :cli_executable, @default_cli_executable)
end
defp get_extra_extensions do
Application.get_env(:mix_test_watch, :extra_extensions, @default_extra_extensions)
end
end
defmodule Mix.Tasks.Test.Watch do
use Mix.Task
@moduledoc """
A task for running tests whenever source files change.
"""
@shortdoc "Automatically run tests on file changes"
@preferred_cli_env :test
defdelegate run(args), to: MixTestWatch
end
{<<"app">>,<<"mix_test_watch">>}.
{<<"build_tools">>,[<<"mix">>]}.
{<<"description">>,<<"Automatically run tests when files change">>}.
{<<"elixir">>,<<"~> 1.0">>}.
{<<"files">>,
[<<"LICENCE">>,<<"README.md">>,<<"CHANGELOG.md">>,<<"lib">>,<<"lib/mix">>,
<<"lib/mix/tasks">>,<<"lib/mix/tasks/test">>,
<<"lib/mix/tasks/test/watch.ex">>,<<"lib/mix_test_watch.ex">>,
<<"lib/mix_test_watch">>,<<"lib/mix_test_watch/config.ex">>,
<<"lib/mix_test_watch/message_inbox.ex">>,<<"lib/mix_test_watch/path.ex">>,
<<"lib/mix_test_watch/port_runner">>,
<<"lib/mix_test_watch/port_runner/port_runner.ex">>,
<<"lib/mix_test_watch/runner.ex">>,<<"lib/mix_test_watch/watcher.ex">>,
<<"priv">>,<<"priv/zombie_killer">>,<<"mix.exs">>]}.
{<<"licenses">>,[<<"MIT">>]}.
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/lpil/mix-test.watch">>}]}.
{<<"name">>,<<"mix_test_watch">>}.
{<<"requirements">>,
[[{<<"app">>,<<"file_system">>},
{<<"name">>,<<"file_system">>},
{<<"optional">>,false},
{<<"repository">>,<<"hexpm">>},
{<<"requirement">>,<<"~> 0.2.1 or ~> 0.3">>}]]}.
{<<"version">>,<<"1.0.2">>}.
mix test.watch
==============
[![Build Status](https://travis-ci.org/lpil/mix-test.watch.svg?branch=master)](https://travis-ci.org/lpil/mix-test.watch)
[![Hex version](https://img.shields.io/hexpm/v/mix_test_watch.svg "Hex version")](https://hex.pm/packages/mix_test_watch)
[![Hex downloads](https://img.shields.io/hexpm/dt/mix_test_watch.svg "Hex downloads")](https://hex.pm/packages/mix_test_watch)
Automatically run your Elixir project's tests each time you save a file.
Because TDD is awesome.
## Usage
Add it to your dependencies:
```elixir
# mix.exs (Elixir 1.4)
def deps do
[{:mix_test_watch, "~> 1.0", only: :dev, runtime: false}]
end
```
```elixir
# mix.exs (Elixir 1.3 and earlier)
def deps do
[{:mix_test_watch, "~> 1.0", only: :dev}]
end
```
Run the mix task
```
mix test.watch
```
Start hacking :)
## Running Additional Mix Tasks
Through the mix config it is possible to run other mix tasks as well as the
test task. For example, if I wished to run the [Dogma][dogma] code style
linter after my tests I would do so like this.
[dogma]: https://github.com/lpil/dogma
```elixir
# config/config.exs
use Mix.Config
if Mix.env == :dev do
config :mix_test_watch,
tasks: [
"test",
"dogma",
]
end
```
Tasks are run in the order they appear in the list, and the progression will
stop if any command returns a non-zero exit code.
All tasks are run with `MIX_ENV` set to `test`.
## Passing Arguments To Tasks
Any command line arguments passed to the `test.watch` task will be passed
through to the tasks being run. If I only want to run the tests from one file
every time I save a file I could do so with this command:
```
mix test.watch test/file/to_test.exs
```
Note that if you have configured more than one task to be run these arguments
will be passed to all the tasks run, not just the test command.
## Running tests of modules that changed
Elixir 1.3 introduced `--stale` option that will run only those test files which reference modules that have changed since the last run. You can pass it to test.watch:
```
mix test.watch --stale
```
## Clearing The Console Before Each Run
If you want mix test.watch to clear the console before each run, you can
enable this option in your config/dev.exs as follows:
```elixir
# config/config.exs
use Mix.Config
if Mix.env == :dev do
config :mix_test_watch,
clear: true
end
```
## Excluding files or directories
To ignore changes from specific files or directories just add `exclude:` regexp
patterns to your config in `mix.exs`:
```elixir
# config/config.exs
use Mix.Config
if Mix.env == :dev do
config :mix_test_watch,
exclude: [~r/db_migration\/.*/,
~r/useless_.*\.exs/]
end
```
The default is `exclude: [~r/\.#/, ~r{priv/repo/migrations}]`.
## Compatibility Notes
On Linux you may need to install `inotify-tools`.
## Desktop Notifications
You can enable desktop notifications with
[ex_unit_notifier](https://github.com/navinpeiris/ex_unit_notifier).
## Licence
```
mix test.watch
Copyright © 2015-present Louis Pilfold
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
The MIT License (MIT)
mix test.watch
Copyright © 2015-present Louis Pilfold. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Changelog
=========
## v1.0.2 - 2019-11-17
- Zombie killer script is run with bash to avoid platform specific issues with
sh implementations.
## v1.0.1 - 2019-10-25
- Include zombie killer script in hex package.
## v1.0.0 - 2019-10-25
- LiveView templates are now watched.
## v0.9.0 - 2018-09-17
- Avoid starting application if `--no-start` is given.
- Hot runner removed.
## v0.8.0 - 2018-07-30
- Application started on test run. Revert of v0.7 behaviour.
## v0.7.0 - 2018-07-35
- No longer start application on test run.
- Do not watch the Ecto migration directory by default.
## v0.6.0 - 2018-03-27
- Switch from `fs` to `file_system` for file system event watching.
## v0.5.0 - 2017-08-26
- Windows support (Rustam @rustamtolipov)
## v0.4.1 - 2017-06-21
- Revert to `fs` v0.9.1 to maintain Phoenix Live Reload compatibility.
https://github.com/phoenixframework/phoenix_live_reload/commit/e54bf6fb301436797ff589e0b76a047bb79b6870
## v0.4.0 - 2017-04-22
- Emacs temporary files can no longer trigger a test run.
## v0.3.3 - 2017-02-08
- Fixed a bug where arguments were not being correctly passed to the
test running BEAM instance.
## v0.3.1 - 2017-02-04
- Fixed race condition bug on OSX where tests would fail to run when
files are changed.
## v0.3.0 - 2017-01-29
- Test runs optionally print a timestamp (Scotty @unclesnottie)
- Paths can be ignored by watcher (Alex Myasoedov @msoedov)
- Paths can be ignored by watcher (Alex Myasoedov @msoedov)
- Ability to specify additional watched file extensions. (Dave Shah @daveshah)
- Erlang `.hrl` header files are now watched.
- The existing VM can now reused for running the tests with the HotRunner.
This gives us Windows support and a performance increase.
Sadly it cannot be used as the default due to a bug in the Elixir compiler.
## v0.2.6 - 2016-02-28
- The terminal can now be cleared between test runs.
(Gerard de Brieder @smeevil)
## v0.2.5 - 2015-12-31
- It is now possible to run addition tasks using mix config.
- Erlang `.xrl` and `.yrl` files are watched. (John Hamelink @johnhamelink)
- The shell command used to run the tasks can be specified (i.e. `iex -S`).
(John Hamelink @johnhamelink)
- Command line arguments are forwarded to tasks being run. (Johan Lind @behe)
## v0.2.3 - 2015-08-23
- The `_build` directory is ignored, as well as `deps/`.
- Erlang `.erl` files are now watched.
## v0.2.2 - 2015-08-22
- Tests now run once immediately after running the mix task. (Johan Lind @behe)
- Porcelain dependancy removed.
- Switched from bash to sh for running shell commands.
defmodule FileSystem.Mixfile do
use Mix.Project
def project do
[ app: :file_system,
version: "0.2.10",
elixir: "~> 1.3",
deps: deps(),
description: "A file system change watcher wrapper based on [fs](https://github.com/synrc/fs)",
source_url: "https://github.com/falood/file_system",
package: package(),
compilers: [:file_system | Mix.compilers()],
aliases: ["compile.file_system": &file_system/1],
docs: [
extras: ["README.md"],
main: "readme",
]
]
end
def application do
[
applications: [:logger],
]
end
defp deps do
[
{ :ex_doc, "~> 0.14", only: :docs },
]
end
defp file_system(_args) do
case :os.type() do
{:unix, :darwin} -> compile_mac()
_ -> :ok
end
end
defp compile_mac do
require Logger
source = "c_src/mac/*.c"
target = "priv/mac_listener"
if Mix.Utils.stale?(Path.wildcard(source), [target]) do
Logger.info "Compiling file system watcher for Mac..."
cmd = "clang -framework CoreFoundation -framework CoreServices -Wno-deprecated-declarations #{source} -o #{target}"
if Mix.shell().cmd(cmd) > 0 do
Logger.error "Could not compile file system watcher for Mac, try to run #{inspect cmd} manually inside the dependency."
else
Logger.info "Done."
end
:ok
else
:noop
end
end
defp package do
%{ maintainers: ["Xiangrong Hao", "Max Veytsman"],
files: [
"lib", "README.md", "mix.exs",
"c_src/mac/cli.c",
"c_src/mac/cli.h",
"c_src/mac/common.h",
"c_src/mac/compat.c",
"c_src/mac/compat.h",
"c_src/mac/main.c",
"priv/inotifywait.exe",
],
licenses: ["WTFPL"],
links: %{"Github" => "https://github.com/falood/file_system"}
}
end
end
defmodule FileSystem do
@moduledoc File.read!("README.md")
@doc """
## Options
* `:dirs` ([string], required), the dir list to monitor
* `:backend` (atom, optional), default backends: `:fs_mac`
for `macos`, `:fs_inotify` for `linux`, `freebsd` and `openbsd`,
`:fs_windows` for `windows`
* `:name` (atom, optional), `name` can be used to subscribe as
the same as pid when the `name` is given. The `name` should
be the name of worker process.
* All rest options will treated as backend options. See backend
module documents for more details.
## Example
Simple usage:
iex> {:ok, pid} = FileSystem.start_link(dirs: ["/tmp/fs"])
iex> FileSystem.subscribe(pid)
Get instant notifications on file changes for Mac OS X:
iex> FileSystem.start_link(dirs: ["/path/to/some/files"], latency: 0)
Named monitor with specified backend:
iex> FileSystem.start_link(backend: :fs_mac, dirs: ["/tmp/fs"], name: :worker)
iex> FileSystem.subscribe(:worker)
"""
@spec start_link(Keyword.t) :: GenServer.on_start()
def start_link(options) do
FileSystem.Worker.start_link(options)
end
@doc """
Register the current process as a subscriber of a file_system worker.
The pid you subscribed from will now receive messages like
{:file_event, worker_pid, {file_path, events}}
{:file_event, worker_pid, :stop}
"""
@spec subscribe(GenServer.server) :: :ok
def subscribe(pid) do
GenServer.call(pid, :subscribe)
end
end
defmodule FileSystem.Worker do
@moduledoc """
FileSystem Worker Process with the backend GenServer, receive events from Port Process
and forward it to subscribers.
"""
use GenServer
@doc false
def start_link(args) do
{opts, args} = Keyword.split(args, [:name])
GenServer.start_link(__MODULE__, args, opts)
end
@doc false
def init(args) do
{backend, rest} = Keyword.pop(args, :backend)
with {:ok, backend} <- FileSystem.Backend.backend(backend),
{:ok, backend_pid} <- backend.start_link([{:worker_pid, self()} | rest])
do
{:ok, %{backend_pid: backend_pid, subscribers: %{}}}
else
_ -> :ignore
end
end
@doc false
def handle_call(:subscribe, {pid, _}, state) do
ref = Process.monitor(pid)
state = put_in(state, [:subscribers, ref], pid)
{:reply, :ok, state}
end
@doc false
def handle_info({:backend_file_event, backend_pid, file_event}, %{backend_pid: backend_pid}=state) do
state.subscribers |> Enum.each(fn {_ref, subscriber_pid} ->
send(subscriber_pid, {:file_event, self(), file_event})
end)
{:noreply, state}
end
def handle_info({:DOWN, ref, _, _pid, _reason}, state) do
subscribers = Map.drop(state.subscribers, [ref])
{:noreply, %{state | subscribers: subscribers}}
end
def handle_info(_, state) do
{:noreply, state}
end
end
require Logger
defmodule FileSystem.Backends.FSWindows do
@moduledoc """
This file is a fork from https://github.com/synrc/fs.
FileSysetm backend for windows, a GenServer receive data from Port, parse event
and send it to the worker process.
Need binary executable file packaged in to use this backend.
## Backend Options
* `:recursive` (bool, default: true), monitor directories and their contents recursively
## Executable File Path
The default executable file is `inotifywait.exe` in `priv` dir of `:file_system` application, there're two ways to custom it, useful when run `:file_system` with escript.
* config with `config.exs`
`config :file_system, :fs_windows, executable_file: "YOUR_EXECUTABLE_FILE_PATH"`
* config with `FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE` os environment
FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE=YOUR_EXECUTABLE_FILE_PATH
"""
use GenServer
@behaviour FileSystem.Backend
@sep_char <<1>>
@default_exec_file "inotifywait.exe"
def bootstrap do
exec_file = executable_path()
if not is_nil(exec_file) and File.exists?(exec_file) do
:ok
else
Logger.error "Can't find executable `inotifywait.exe`"
{:error, :fs_windows_bootstrap_error}
end
end
def supported_systems do
[{:win32, :nt}]
end
def known_events do
[:created, :modified, :removed, :renamed, :undefined]
end
defp executable_path do
executable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || executable_path(:priv)
end
defp executable_path(:config) do
Application.get_env(:file_system, :fs_windows)[:executable_file]
end
defp executable_path(:system_env) do
System.get_env("FILESYSTEM_FSMWINDOWS_EXECUTABLE_FILE")
end
defp executable_path(:system_path) do
System.find_executable(@default_exec_file)
end
defp executable_path(:priv) do
case :code.priv_dir(:file_system) do
{:error, _} ->
Logger.error "`priv` dir for `:file_system` application is not avalible in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE` env."
nil
dir when is_list(dir) ->
Path.join(dir, @default_exec_file)
end
end
def parse_options(options) do
case Keyword.pop(options, :dirs) do
{nil, _} ->
Logger.error "required argument `dirs` is missing"
{:error, :missing_dirs_argument}
{dirs, rest} ->
format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist
args = [
'--format', format, '--quiet', '-m', '-r'
| dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)
]
parse_options(rest, args)
end
end
defp parse_options([], result), do: {:ok, result}
defp parse_options([{:recursive, true} | t], result) do
parse_options(t, result)
end
defp parse_options([{:recursive, false} | t], result) do
parse_options(t, result -- ['-r'])
end
defp parse_options([{:recursive, value} | t], result) do
Logger.error "unknown value `#{inspect value}` for recursive, ignore"
parse_options(t, result)
end
defp parse_options([h | t], result) do
Logger.error "unknown option `#{inspect h}`, ignore"
parse_options(t, result)
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, [])
end
def init(args) do
{worker_pid, rest} = Keyword.pop(args, :worker_pid)
case parse_options(rest) do
{:ok, port_args} ->
port = Port.open(
{:spawn_executable, to_charlist(executable_path())},
[:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}]
)
Process.link(port)
Process.flag(:trap_exit, true)
{:ok, %{port: port, worker_pid: worker_pid}}
{:error, _} ->
:ignore
end
end
def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do
{file_path, events} = line |> parse_line
send(state.worker_pid, {:backend_file_event, self(), {file_path, events}})
{:noreply, state}
end
def handle_info({port, {:exit_status, _}}, %{port: port}=state) do
send(state.worker_pid, {:backend_file_event, self(), :stop})
{:stop, :normal, state}
end
def handle_info({:EXIT, port, _reason}, %{port: port}=state) do
send(state.worker_pid, {:backend_file_event, self(), :stop})
{:stop, :normal, state}
end
def handle_info(_, state) do
{:noreply, state}
end
def parse_line(line) do
{path, flags} =
case line |> to_string |> String.split(@sep_char, trim: true) do
[dir, flags, file] -> {Enum.join([dir, file], "\\"), flags}
[path, flags] -> {path, flags}
end
{path |> Path.split() |> Path.join(), flags |> String.split(",") |> Enum.map(&convert_flag/1)}
end
defp convert_flag("CREATE"), do: :created
defp convert_flag("MODIFY"), do: :modified
defp convert_flag("DELETE"), do: :removed
defp convert_flag("MOVED_TO"), do: :renamed
defp convert_flag(_), do: :undefined
end
require Logger
defmodule FileSystem.Backends.FSPoll do
@moduledoc """
FileSysetm backend for any OS, a GenServer that regularly scans file system to
detect changes and send them to the worker process.
## Backend Options
* `:interval` (integer, default: 1000), polling interval
## Use FSPoll Backend
Unlike other backends, polling backend is never automatically chosen in any
OS environment, despite being usable on all platforms.
To use polling backend, one has to explicitly specify in the backend option.
"""
use GenServer
@behaviour FileSystem.Backend
def bootstrap, do: :ok
def supported_systems do
[{:unix, :linux}, {:unix, :freebsd}, {:unix, :openbsd}, {:unix, :darwin}, {:win32, :nt}]
end
def known_events do
[:created, :deleted, :modified]
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, [])
end
def init(args) do
worker_pid = Keyword.fetch!(args, :worker_pid)
dirs = Keyword.fetch!(args, :dirs)
interval = Keyword.get(args, :interval, 1000)
Logger.info("Polling file changes every #{interval}ms...")
send(self(), :first_check)
{:ok, {worker_pid, dirs, interval, %{}}}
end
def handle_info(:first_check, {worker_pid, dirs, interval, _empty_map}) do
schedule_check(interval)
{:noreply, {worker_pid, dirs, interval, files_mtimes(dirs)}}
end
def handle_info(:check, {worker_pid, dirs, interval, stale_mtimes}) do
fresh_mtimes = files_mtimes(dirs)
diff(stale_mtimes, fresh_mtimes)
|> Tuple.to_list
|> Enum.zip([:created, :deleted, :modified])
|> Enum.each(&report_change(&1, worker_pid))
schedule_check(interval)
{:noreply, {worker_pid, dirs, interval, fresh_mtimes}}
end
defp schedule_check(interval) do
Process.send_after(self(), :check, interval)
end
defp files_mtimes(dirs, files_mtimes_map \\ %{}) do
Enum.reduce(dirs, files_mtimes_map, fn dir, map ->
case File.stat!(dir) do
%{type: :regular, mtime: mtime} ->
Map.put(map, dir, mtime)
%{type: :directory} ->
dir
|> Path.join("*")
|> Path.wildcard
|> files_mtimes(map)
%{type: _other} ->
map
end
end)
end
@doc false
def diff(stale_mtimes, fresh_mtimes) do
fresh_file_paths = fresh_mtimes |> Map.keys |> MapSet.new
stale_file_paths = stale_mtimes |> Map.keys |> MapSet.new
created_file_paths =
MapSet.difference(fresh_file_paths, stale_file_paths) |> MapSet.to_list
deleted_file_paths =
MapSet.difference(stale_file_paths, fresh_file_paths) |> MapSet.to_list
modified_file_paths =
for file_path <- MapSet.intersection(stale_file_paths, fresh_file_paths),
stale_mtimes[file_path] != fresh_mtimes[file_path], do: file_path
{created_file_paths, deleted_file_paths, modified_file_paths}
end
defp report_change({file_paths, event}, worker_pid) do
for file_path <- file_paths do
send(worker_pid, {:backend_file_event, self(), {file_path, [event]}})
end
end
end
require Logger
defmodule FileSystem.Backends.FSMac do
@moduledoc """
This file is a fork from https://github.com/synrc/fs.
FileSysetm backend for macos, a GenServer receive data from Port, parse event
and send it to the worker process.
Will compile executable the buildin executable file when file the first time it is used.
## Backend Options
* `:latency` (float, default: 0.5), latency period.
* `:no_defer` (bool, default: false), enable no-defer latency modifier.
Works with latency parameter, Also check apple `FSEvent` api documents
https://developer.apple.com/documentation/coreservices/kfseventstreamcreateflagnodefer
* `:watch_root` (bool, default: false), watch for when the root path has changed.
Set the flag `true` to monitor events when watching `/tmp/fs/dir` and run
`mv /tmp/fs /tmp/fx`. Also check apple `FSEvent` api documents
https://developer.apple.com/documentation/coreservices/kfseventstreamcreateflagwatchroot
* recursive is enabled by default, no option to disable it for now.
## Executable File Path
The default executable file is `mac_listener` in `priv` dir of `:file_system` application, there're two ways to custom it, useful when run `:file_system` with escript.
* config with `config.exs`
`config :file_system, :fs_mac, executable_file: "YOUR_EXECUTABLE_FILE_PATH"`
* config with `FILESYSTEM_FSMAC_EXECUTABLE_FILE` os environment
FILESYSTEM_FSMAC_EXECUTABLE_FILE=YOUR_EXECUTABLE_FILE_PATH
"""
use GenServer
@behaviour FileSystem.Backend
@default_exec_file "mac_listener"
def bootstrap do
exec_file = executable_path()
if not is_nil(exec_file) and File.exists?(exec_file) do
:ok
else
Logger.error "Can't find executable `mac_listener`"
{:error, :fs_mac_bootstrap_error}
end
end
def supported_systems do
[{:unix, :darwin}]
end
def known_events do
[ :mustscansubdirs, :userdropped, :kerneldropped, :eventidswrapped, :historydone,
:rootchanged, :mount, :unmount, :created, :removed, :inodemetamod, :renamed, :modified,
:finderinfomod, :changeowner, :xattrmod, :isfile, :isdir, :issymlink, :ownevent,
]
end
defp executable_path do
executable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || executable_path(:priv)
end
defp executable_path(:config) do
Application.get_env(:file_system, :fs_mac)[:executable_file]
end
defp executable_path(:system_env) do
System.get_env("FILESYSTEM_FSMAC_EXECUTABLE_FILE")
end
defp executable_path(:system_path) do
System.find_executable(@default_exec_file)
end
defp executable_path(:priv) do
case :code.priv_dir(:file_system) do
{:error, _} ->
Logger.error "`priv` dir for `:file_system` application is not avalible in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSMAC_EXECUTABLE_FILE` env."
nil
dir when is_list(dir) ->
Path.join(dir, @default_exec_file)
end
end
def parse_options(options) do
case Keyword.pop(options, :dirs) do
{nil, _} ->
Logger.error "required argument `dirs` is missing"
{:error, :missing_dirs_argument}
{dirs, rest} ->
args = ['-F' | dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)]
parse_options(rest, args)
end
end
defp parse_options([], result), do: {:ok, result}
defp parse_options([{:latency, latency} | t], result) do
result =
if is_float(latency) or is_integer(latency) do
['--latency=#{latency / 1}' | result]
else
Logger.error "latency should be integer or float, got `#{inspect latency}, ignore"
result
end
parse_options(t, result)
end
defp parse_options([{:no_defer, true} | t], result) do
parse_options(t, ['--no-defer' | result])
end
defp parse_options([{:no_defer, false} | t], result) do
parse_options(t, result)
end
defp parse_options([{:no_defer, value} | t], result) do
Logger.error "unknown value `#{inspect value}` for no_defer, ignore"
parse_options(t, result)
end
defp parse_options([{:with_root, true} | t], result) do
parse_options(t, ['--with-root' | result])
end
defp parse_options([{:with_root, false} | t], result) do
parse_options(t, result)
end
defp parse_options([{:with_root, value} | t], result) do
Logger.error "unknown value `#{inspect value}` for with_root, ignore"
parse_options(t, result)
end
defp parse_options([h | t], result) do
Logger.error "unknown option `#{inspect h}`, ignore"
parse_options(t, result)
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, [])
end
def init(args) do
{worker_pid, rest} = Keyword.pop(args, :worker_pid)
case parse_options(rest) do
{:ok, port_args} ->
port = Port.open(
{:spawn_executable, to_charlist(executable_path())},
[:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}]
)
Process.link(port)
Process.flag(:trap_exit, true)
{:ok, %{port: port, worker_pid: worker_pid}}
{:error, _} ->
:ignore
end
end
def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do
{file_path, events} = line |> parse_line
send(state.worker_pid, {:backend_file_event, self(), {file_path, events}})
{:noreply, state}
end
def handle_info({port, {:exit_status, _}}, %{port: port}=state) do
send(state.worker_pid, {:backend_file_event, self(), :stop})
{:stop, :normal, state}
end
def handle_info({:EXIT, port, _reason}, %{port: port}=state) do
send(state.worker_pid, {:backend_file_event, self(), :stop})
{:stop, :normal, state}
end
def handle_info(_, state) do
{:noreply, state}
end
def parse_line(line) do
[_, _, events, path] = line |> to_string |> String.split(["\t", "="], parts: 4)
{path, events |> String.split(["[", ",", "]"], trim: true) |> Enum.map(&String.to_existing_atom/1)}
end
end
require Logger
defmodule FileSystem.Backends.FSInotify do
@moduledoc """
This file is a fork from https://github.com/synrc/fs.
FileSystem backend for linux, freebsd and openbsd, a GenServer receive data from Port, parse event
and send it to the worker process.
Need `inotify-tools` installed to use this backend.
## Backend Options
* `:recursive` (bool, default: true), monitor directories and their contents recursively
## Executable File Path
The default behaivour to find executable file is finding `inotifywait` from `$PATH`, there're two ways to custom it, useful when run `:file_system` with escript.
* config with `config.exs`
`config :file_system, :fs_inotify, executable_file: "YOUR_EXECUTABLE_FILE_PATH"`
* config with `FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE` os environment
FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE=YOUR_EXECUTABLE_FILE_PATH
"""
use GenServer
@behaviour FileSystem.Backend
@sep_char <<1>>
def bootstrap do
exec_file = executable_path()
if is_nil(exec_file) do
Logger.error "`inotify-tools` is needed to run `file_system` for your system, check https://github.com/rvoicilas/inotify-tools/wiki for more information about how to install it. If it's already installed but not be found, appoint executable file with `config.exs` or `FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE` env."
{:error, :fs_inotify_bootstrap_error}
else
:ok
end
end
def supported_systems do
[{:unix, :linux}, {:unix, :freebsd}, {:unix, :openbsd}]
end
def known_events do
[:created, :deleted, :closed, :modified, :isdir, :attribute, :undefined]
end
defp executable_path do
executable_path(:system_env) || executable_path(:config) || executable_path(:system_path)
end
defp executable_path(:config) do
Application.get_env(:file_system, :fs_inotify)[:executable_file]
end
defp executable_path(:system_env) do
System.get_env("FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE")
end
defp executable_path(:system_path) do
System.find_executable("inotifywait")
end
def parse_options(options) do
case Keyword.pop(options, :dirs) do
{nil, _} ->
Logger.error "required argument `dirs` is missing"
{:error, :missing_dirs_argument}
{dirs, rest} ->
format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist
args = [
'-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'moved_from',
'-e', 'create', '-e', 'delete', '-e', 'attrib', '--format', format, '--quiet', '-m', '-r'
| dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)
]
parse_options(rest, args)
end
end
defp parse_options([], result), do: {:ok, result}
defp parse_options([{:recursive, true} | t], result) do
parse_options(t, result)
end
defp parse_options([{:recursive, false} | t], result) do
parse_options(t, result -- ['-r'])
end
defp parse_options([{:recursive, value} | t], result) do
Logger.error "unknown value `#{inspect value}` for recursive, ignore"
parse_options(t, result)
end
defp parse_options([h | t], result) do
Logger.error "unknown option `#{inspect h}`, ignore"
parse_options(t, result)
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, [])
end
def init(args) do
{worker_pid, rest} = Keyword.pop(args, :worker_pid)
case parse_options(rest) do
{:ok, port_args} ->
bash_args = ['-c', '#{executable_path()} "$0" "$@" & PID=$!; read a; kill -KILL $PID']
all_args =
case :os.type() do
{:unix, :freebsd} ->
bash_args ++ ['--'] ++ port_args
_ ->
bash_args ++ port_args
end
port = Port.open(
{:spawn_executable, '/bin/sh'},
[:stream, :exit_status, {:line, 16384}, {:args, all_args}, {:cd, System.tmp_dir!()}]
)
Process.link(port)
Process.flag(:trap_exit, true)
{:ok, %{port: port, worker_pid: worker_pid}}
{:error, _} ->
:ignore
end
end
def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do
{file_path, events} = line |> parse_line
send(state.worker_pid, {:backend_file_event, self(), {file_path, events}})
{:noreply, state}
end
def handle_info({port, {:exit_status, _}}, %{port: port}=state) do
send(state.worker_pid, {:backend_file_event, self(), :stop})
{:stop, :normal, state}
end
def handle_info({:EXIT, port, _reason}, %{port: port}=state) do
send(state.worker_pid, {:backend_file_event, self(), :stop})
{:stop, :normal, state}
end
def handle_info(_, state) do
{:noreply, state}
end
def parse_line(line) do
{path, flags} =
case line |> to_string |> String.split(@sep_char, trim: true) do
[dir, flags, file] -> {Path.join(dir, file), flags}
[path, flags] -> {path, flags}
end
{path, flags |> String.split(",") |> Enum.map(&convert_flag/1)}
end
defp convert_flag("CREATE"), do: :created
defp convert_flag("MOVED_TO"), do: :moved_to
defp convert_flag("DELETE"), do: :deleted
defp convert_flag("MOVED_FROM"), do: :moved_from
defp convert_flag("ISDIR"), do: :isdir
defp convert_flag("MODIFY"), do: :modified
defp convert_flag("CLOSE_WRITE"), do: :modified
defp convert_flag("CLOSE"), do: :closed
defp convert_flag("ATTRIB"), do: :attribute
defp convert_flag(_), do: :undefined
end
require Logger
defmodule FileSystem.Backend do
@moduledoc """
FileSystem Backend Behaviour.
"""
@callback bootstrap() :: :ok | {:error, atom()}
@callback supported_systems() :: [{atom(), atom()}]
@callback known_events() :: [atom()]
@doc """
Get and validate backend module, return `{:ok, backend_module}` when success and
return `{:error, reason}` when fail.
When `nil` is given, will return default backend by os.
When a custom module is given, make sure `start_link/1`, `bootstrap/0` and
`supported_system/0` are defnied.
"""
@spec backend(atom) :: {:ok, atom()} | {:error, atom()}
def backend(backend) do
with {:ok, module} <- backend_module(backend),
:ok <- validate_os(backend, module),
:ok <- module.bootstrap
do
{:ok, module}
else
{:error, reason} -> {:error, reason}
end
end
defp backend_module(nil) do
case :os.type() do
{:unix, :darwin} -> :fs_mac
{:unix, :linux} -> :fs_inotify
{:unix, :freebsd} -> :fs_inotify
{:unix, :openbsd} -> :fs_inotify
{:win32, :nt} -> :fs_windows
system -> {:unsupported_system, system}
end |> backend_module
end
defp backend_module(:fs_mac), do: {:ok, FileSystem.Backends.FSMac}
defp backend_module(:fs_inotify), do: {:ok, FileSystem.Backends.FSInotify}
defp backend_module(:fs_windows), do: {:ok, FileSystem.Backends.FSWindows}
defp backend_module(:fs_poll), do: {:ok, FileSystem.Backends.FSPoll}
defp backend_module({:unsupported_system, system}) do
Logger.error "I'm so sorry but `file_system` does NOT support your current system #{inspect system} for now."
{:error, :unsupported_system}
end
defp backend_module(module) do
functions = module.__info__(:functions)
{:start_link, 1} in functions &&
{:bootstrap, 0} in functions &&
{:supported_systems, 0} in functions ||
raise "illegal backend"
rescue
_ ->
Logger.error "You are using custom backend `#{inspect module}`, make sure it's a legal file_system backend module."
{:error, :illegal_backend}
end
defp validate_os(backend, module) do
os_type = :os.type()
if os_type in module.supported_systems() do
:ok
else
Logger.error "The backend `#{backend}` you are using does NOT support your current system #{inspect os_type}."
{:error, :unsupported_system}
end
end
end
{<<"app">>,<<"file_system">>}.
{<<"build_tools">>,[<<"mix">>]}.
{<<"description">>,
<<"A file system change watcher wrapper based on [fs](https://github.com/synrc/fs)">>}.
{<<"elixir">>,<<"~> 1.3">>}.
{<<"files">>,
[<<"lib">>,<<"lib/file_system">>,<<"lib/file_system/worker.ex">>,
<<"lib/file_system/backends">>,<<"lib/file_system/backends/fs_inotify.ex">>,
<<"lib/file_system/backends/fs_mac.ex">>,
<<"lib/file_system/backends/fs_poll.ex">>,
<<"lib/file_system/backends/fs_windows.ex">>,
<<"lib/file_system/backend.ex">>,<<"lib/file_system.ex">>,<<"README.md">>,
<<"mix.exs">>,<<"c_src/mac/cli.c">>,<<"c_src/mac/cli.h">>,
<<"c_src/mac/common.h">>,<<"c_src/mac/compat.c">>,<<"c_src/mac/compat.h">>,
<<"c_src/mac/main.c">>,<<"priv/inotifywait.exe">>]}.
{<<"licenses">>,[<<"WTFPL">>]}.
{<<"links">>,[{<<"Github">>,<<"https://github.com/falood/file_system">>}]}.
{<<"name">>,<<"file_system">>}.
{<<"requirements">>,[]}.
{<<"version">>,<<"0.2.10">>}.
#include "common.h"
#include "cli.h"
// TODO: set on fire. cli.{h,c} handle both parsing and defaults, so there's
// no need to set those here. also, in order to scope metadata by path,
// each stream will need its own configuration... so this won't work as
// a global any more. In the end the goal is to make the output format
// able to declare not just that something happened and what flags were
// attached, but what path it was watching that caused those events (so
// that the path itself can be used for routing that information to the
// relevant callback).
//
// Structure for storing metadata parsed from the commandline
static struct {
FSEventStreamEventId sinceWhen;
CFTimeInterval latency;
FSEventStreamCreateFlags flags;
CFMutableArrayRef paths;
int format;
} config = {
(UInt64) kFSEventStreamEventIdSinceNow,
(double) 0.3,
(CFOptionFlags) kFSEventStreamCreateFlagNone,
NULL,
0
};
// Prototypes
static void append_path(const char* path);
static inline void parse_cli_settings(int argc, const char* argv[]);
static void callback(FSEventStreamRef streamRef,
void* clientCallBackInfo,
size_t numEvents,
void* eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[]);
static void append_path(const char* path)
{
CFStringRef pathRef = CFStringCreateWithCString(kCFAllocatorDefault,
path,
kCFStringEncodingUTF8);
CFArrayAppendValue(config.paths, pathRef);
CFRelease(pathRef);
}
// Parse commandline settings
static inline void parse_cli_settings(int argc, const char* argv[])
{
// runtime os version detection
SInt32 osMajorVersion, osMinorVersion;
if (!(Gestalt(gestaltSystemVersionMajor, &osMajorVersion) == noErr)) {
osMajorVersion = 0;
}
if (!(Gestalt(gestaltSystemVersionMinor, &osMinorVersion) == noErr)) {
osMinorVersion = 0;
}
if ((osMajorVersion == 10) & (osMinorVersion < 5)) {
fprintf(stderr, "The FSEvents API is unavailable on this version of macos!\n");
exit(EXIT_FAILURE);
}
struct cli_info args_info;
cli_parser_init(&args_info);
if (cli_parser(argc, argv, &args_info) != 0) {
exit(EXIT_FAILURE);
}
config.paths = CFArrayCreateMutable(NULL,
(CFIndex)0,
&kCFTypeArrayCallBacks);
config.sinceWhen = args_info.since_when_arg;
config.latency = args_info.latency_arg;
config.format = args_info.format_arg;
if (args_info.no_defer_flag) {
config.flags |= kFSEventStreamCreateFlagNoDefer;
}
if (args_info.watch_root_flag) {
config.flags |= kFSEventStreamCreateFlagWatchRoot;
}
if (args_info.ignore_self_flag) {
if ((osMajorVersion > 10) | ((osMajorVersion == 10) & (osMinorVersion >= 6))) {
config.flags |= kFSEventStreamCreateFlagIgnoreSelf;
} else {
fprintf(stderr, "MacOSX 10.6 or later is required for --ignore-self\n");
exit(EXIT_FAILURE);
}
}
if (args_info.file_events_flag) {
if ((osMajorVersion > 10) | ((osMajorVersion == 10) & (osMinorVersion >= 7))) {
config.flags |= kFSEventStreamCreateFlagFileEvents;
} else {
fprintf(stderr, "MacOSX 10.7 or later required for --file-events\n");
exit(EXIT_FAILURE);
}
}
if (args_info.mark_self_flag) {
if ((osMajorVersion > 10) | ((osMajorVersion == 10) & (osMinorVersion >= 9))) {
config.flags |= kFSEventStreamCreateFlagMarkSelf;
} else {
fprintf(stderr, "MacOSX 10.9 or later required for --mark-self\n");
exit(EXIT_FAILURE);
}
}
if (args_info.inputs_num == 0) {
append_path(".");
} else {
for (unsigned int i=0; i < args_info.inputs_num; ++i) {
append_path(args_info.inputs[i]);
}
}
cli_parser_free(&args_info);
#ifdef DEBUG
fprintf(stderr, "config.sinceWhen %llu\n", config.sinceWhen);
fprintf(stderr, "config.latency %f\n", config.latency);
fprintf(stderr, "config.flags %#.8x\n", config.flags);
FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagUseCFTypes,
" Using CF instead of C types");
FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagNoDefer,
" NoDefer latency modifier enabled");
FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagWatchRoot,
" WatchRoot notifications enabled");
FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagIgnoreSelf,
" IgnoreSelf enabled");
FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagFileEvents,
" FileEvents enabled");
fprintf(stderr, "config.paths\n");
long numpaths = CFArrayGetCount(config.paths);
for (long i = 0; i < numpaths; i++) {
char path[PATH_MAX];
CFStringGetCString(CFArrayGetValueAtIndex(config.paths, i),
path,
PATH_MAX,
kCFStringEncodingUTF8);
fprintf(stderr, " %s\n", path);
}
fprintf(stderr, "\n");
#endif
}
static void callback(__attribute__((unused)) FSEventStreamRef streamRef,
__attribute__((unused)) void* clientCallBackInfo,
size_t numEvents,
void* eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[])
{
char** paths = eventPaths;
char *buf = calloc(sizeof(FSEVENTSBITS), sizeof(char));
for (size_t i = 0; i < numEvents; i++) {
sprintb(buf, eventFlags[i], FSEVENTSBITS);
printf("%llu\t%#.8x=[%s]\t%s\n", eventIds[i], eventFlags[i], buf, paths[i]);
}
fflush(stdout);
free(buf);
if (fcntl(STDIN_FILENO, F_GETFD) == -1) {
CFRunLoopStop(CFRunLoopGetCurrent());
}
}
static void stdin_callback(CFFileDescriptorRef fdref, CFOptionFlags callBackTypes, void *info)
{
char buf[1024];
int nread;
do {
nread = read(STDIN_FILENO, buf, sizeof(buf));
if (nread == -1 && errno == EAGAIN) {
CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
return;
} else if (nread == 0) {
exit(1);
return;
}
} while (nread > 0);
}
int main(int argc, const char* argv[])
{
parse_cli_settings(argc, argv);
FSEventStreamContext context = {0, NULL, NULL, NULL, NULL};
FSEventStreamRef stream;
stream = FSEventStreamCreate(kCFAllocatorDefault,
(FSEventStreamCallback)&callback,
&context,
config.paths,
config.sinceWhen,
config.latency,
config.flags);
#ifdef DEBUG
FSEventStreamShow(stream);
fprintf(stderr, "\n");
#endif
fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK);
CFFileDescriptorRef fdref = CFFileDescriptorCreate(kCFAllocatorDefault, STDIN_FILENO, false, stdin_callback, NULL);
CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
FSEventStreamScheduleWithRunLoop(stream,
CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode);
FSEventStreamStart(stream);
CFRunLoopRun();
FSEventStreamFlushSync(stream);
FSEventStreamStop(stream);
return 0;
}
// vim: ts=2 sts=2 et sw=2
/**
* @headerfile compat.h
* FSEventStream flag compatibility shim
*
* In order to compile a binary against an older SDK yet still support the
* features present in later OS releases, we need to define any missing enum
* constants not present in the older SDK. This allows us to safely defer
* feature detection to runtime (and avoid recompilation).
*/
#ifndef fsevent_watch_compat_h
#define fsevent_watch_compat_h
#ifndef __CORESERVICES__
#include <CoreServices/CoreServices.h>
#endif // __CORESERVICES__
#if MAC_OS_X_VERSION_MAX_ALLOWED < 1060
// ignoring events originating from the current process introduced in 10.6
extern FSEventStreamCreateFlags kFSEventStreamCreateFlagIgnoreSelf;
#endif
#if MAC_OS_X_VERSION_MAX_ALLOWED < 1070
// file-level events introduced in 10.7
extern FSEventStreamCreateFlags kFSEventStreamCreateFlagFileEvents;
extern FSEventStreamEventFlags kFSEventStreamEventFlagItemCreated,
kFSEventStreamEventFlagItemRemoved,
kFSEventStreamEventFlagItemInodeMetaMod,
kFSEventStreamEventFlagItemRenamed,
kFSEventStreamEventFlagItemModified,
kFSEventStreamEventFlagItemFinderInfoMod,
kFSEventStreamEventFlagItemChangeOwner,
kFSEventStreamEventFlagItemXattrMod,
kFSEventStreamEventFlagItemIsFile,
kFSEventStreamEventFlagItemIsDir,
kFSEventStreamEventFlagItemIsSymlink;
#endif
#if MAC_OS_X_VERSION_MAX_ALLOWED < 1090
// marking, rather than ignoring, events originating from the current process introduced in 10.9
extern FSEventStreamCreateFlags kFSEventStreamCreateFlagMarkSelf;
extern FSEventStreamEventFlags kFSEventStreamEventFlagOwnEvent;
#endif
#endif // fsevent_watch_compat_h
#include "compat.h"
#if MAC_OS_X_VERSION_MAX_ALLOWED < 1060
FSEventStreamCreateFlags kFSEventStreamCreateFlagIgnoreSelf = 0x00000008;
#endif
#if MAC_OS_X_VERSION_MAX_ALLOWED < 1070
FSEventStreamCreateFlags kFSEventStreamCreateFlagFileEvents = 0x00000010;
FSEventStreamEventFlags kFSEventStreamEventFlagItemCreated = 0x00000100;
FSEventStreamEventFlags kFSEventStreamEventFlagItemRemoved = 0x00000200;
FSEventStreamEventFlags kFSEventStreamEventFlagItemInodeMetaMod = 0x00000400;
FSEventStreamEventFlags kFSEventStreamEventFlagItemRenamed = 0x00000800;
FSEventStreamEventFlags kFSEventStreamEventFlagItemModified = 0x00001000;
FSEventStreamEventFlags kFSEventStreamEventFlagItemFinderInfoMod = 0x00002000;
FSEventStreamEventFlags kFSEventStreamEventFlagItemChangeOwner = 0x00004000;
FSEventStreamEventFlags kFSEventStreamEventFlagItemXattrMod = 0x00008000;
FSEventStreamEventFlags kFSEventStreamEventFlagItemIsFile = 0x00010000;
FSEventStreamEventFlags kFSEventStreamEventFlagItemIsDir = 0x00020000;
FSEventStreamEventFlags kFSEventStreamEventFlagItemIsSymlink = 0x00040000;
#endif
#if MAC_OS_X_VERSION_MAX_ALLOWED < 1090
FSEventStreamCreateFlags kFSEventStreamCreateFlagMarkSelf = 0x00000020;
FSEventStreamEventFlags kFSEventStreamEventFlagOwnEvent = 0x00080000;
#endif
#ifndef fsevent_watch_common_h
#define fsevent_watch_common_h
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#include <unistd.h>
#include <fcntl.h>
#include "compat.h"
#define _str(s) #s
#define _xstr(s) _str(s)
#define COMPILED_AT __DATE__ " " __TIME__
#define FPRINTF_FLAG_CHECK(flags, flag, msg, fd) \
do { \
if ((flags) & (flag)) { \
fprintf(fd, "%s\n", msg); } } \
while (0)
#define FLAG_CHECK_STDERR(flags, flag, msg) \
FPRINTF_FLAG_CHECK(flags, flag, msg, stderr)
/*
* FSEVENTSBITS:
* generated by `make printflags` (and pasted here)
* flags MUST be ordered (bits ascending) and sorted
*
* idea from: http://www.openbsd.org/cgi-bin/cvsweb/src/sbin/ifconfig/ifconfig.c (see printb())
*/
#define FSEVENTSBITS \
"\1mustscansubdirs\2userdropped\3kerneldropped\4eventidswrapped\5historydone\6rootchanged\7mount\10unmount\11created\12removed\13inodemetamod\14renamed\15modified\16finderinfomod\17changeowner\20xattrmod\21isfile\22isdir\23issymlink\24ownevent"
static inline void
sprintb(char *buf, unsigned short v, char *bits)
{
int i, any = 0;
char c;
char *bufp = buf;
while ((i = *bits++)) {
if (v & (1 << (i-1))) {
if (any)
*bufp++ = ',';
any = 1;
for (; (c = *bits) > 32; bits++)
*bufp++ = c;
} else
for (; *bits > 32; bits++)
;
}
*bufp = '\0';
}
#endif /* fsevent_watch_common_h */
#ifndef CLI_H
#define CLI_H
#include "common.h"
#ifndef CLI_NAME
#define CLI_NAME "fsevent_watch"
#endif /* CLI_NAME */
struct cli_info {
UInt64 since_when_arg;
double latency_arg;
bool no_defer_flag;
bool watch_root_flag;
bool ignore_self_flag;
bool file_events_flag;
bool mark_self_flag;
int format_arg;
char** inputs;
unsigned inputs_num;
};
extern const char* cli_info_purpose;
extern const char* cli_info_usage;
extern const char* cli_info_help[];
void cli_print_help(void);
void cli_print_version(void);
int cli_parser (int argc, const char** argv, struct cli_info* args_info);
void cli_parser_init (struct cli_info* args_info);
void cli_parser_free (struct cli_info* args_info);
#endif /* CLI_H */
#include <getopt.h>
#include "cli.h"
const char* cli_info_purpose = "A flexible command-line interface for the FSEvents API";
const char* cli_info_usage = "Usage: fsevent_watch [OPTIONS]... [PATHS]...";
const char* cli_info_help[] = {
" -h, --help you're looking at it",
" -V, --version print version number and exit",
" -p, --show-plist display the embedded Info.plist values",
" -s, --since-when=EventID fire historical events since ID",
" -l, --latency=seconds latency period (default='0.5')",
" -n, --no-defer enable no-defer latency modifier",
" -r, --watch-root watch for when the root path has changed",
// " -i, --ignore-self ignore current process",
" -F, --file-events provide file level event data",
" -f, --format=name output format (ignored)",
0
};
static void default_args (struct cli_info* args_info)
{
args_info->since_when_arg = kFSEventStreamEventIdSinceNow;
args_info->latency_arg = 0.5;
args_info->no_defer_flag = false;
args_info->watch_root_flag = false;
args_info->ignore_self_flag = false;
args_info->file_events_flag = false;
args_info->mark_self_flag = false;
args_info->format_arg = 0;
}
static void cli_parser_release (struct cli_info* args_info)
{
unsigned int i;
for (i=0; i < args_info->inputs_num; ++i) {
free(args_info->inputs[i]);
}
if (args_info->inputs_num) {
free(args_info->inputs);
}
args_info->inputs_num = 0;
}
void cli_parser_init (struct cli_info* args_info)
{
default_args(args_info);
args_info->inputs = 0;
args_info->inputs_num = 0;
}
void cli_parser_free (struct cli_info* args_info)
{
cli_parser_release(args_info);
}
static void cli_print_info_dict (const void *key,
const void *value,
void *context)
{
CFStringRef entry = CFStringCreateWithFormat(NULL, NULL,
CFSTR("%@:\n %@"), key, value);
if (entry) {
CFShow(entry);
CFRelease(entry);
}
}
void cli_show_plist (void)
{
CFBundleRef mainBundle = CFBundleGetMainBundle();
CFRetain(mainBundle);
CFDictionaryRef mainBundleDict = CFBundleGetInfoDictionary(mainBundle);
if (mainBundleDict) {
CFRetain(mainBundleDict);
printf("Embedded Info.plist metadata:\n\n");
CFDictionaryApplyFunction(mainBundleDict, cli_print_info_dict, NULL);
CFRelease(mainBundleDict);
}
CFRelease(mainBundle);
printf("\n");
}
void cli_print_version (void)
{
printf("%s %s\n\n", "VXZ", "1.0");
}
void cli_print_help (void)
{
cli_print_version();
printf("\n%s\n", cli_info_purpose);
printf("\n%s\n", cli_info_usage);
printf("\n");
int i = 0;
while (cli_info_help[i]) {
printf("%s\n", cli_info_help[i++]);
}
}
int cli_parser (int argc, const char** argv, struct cli_info* args_info)
{
static struct option longopts[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'V' },
{ "show-plist", no_argument, NULL, 'p' },
{ "since-when", required_argument, NULL, 's' },
{ "latency", required_argument, NULL, 'l' },
{ "no-defer", no_argument, NULL, 'n' },
{ "watch-root", no_argument, NULL, 'r' },
{ "ignore-self", no_argument, NULL, 'i' },
{ "file-events", no_argument, NULL, 'F' },
{ "mark-self", no_argument, NULL, 'm' },
{ "format", required_argument, NULL, 'f' },
{ 0, 0, 0, 0 }
};
const char* shortopts = "hVps:l:nriFf:";
int c = -1;
while ((c = getopt_long(argc, (char * const*)argv, shortopts, longopts, NULL)) != -1) {
switch(c) {
case 's': // since-when
args_info->since_when_arg = strtoull(optarg, NULL, 0);
break;
case 'l': // latency
args_info->latency_arg = strtod(optarg, NULL);
break;
case 'n': // no-defer
args_info->no_defer_flag = true;
break;
case 'r': // watch-root
args_info->watch_root_flag = true;
break;
case 'i': // ignore-self
args_info->ignore_self_flag = true;
break;
case 'F': // file-events
args_info->file_events_flag = true;
break;
case 'm': // mark-self
args_info->mark_self_flag = true;
break;
case 'f': // format
// XXX: ignored
break;
case 'V': // version
cli_print_version();
exit(EXIT_SUCCESS);
case 'p': // show-plist
cli_show_plist();
exit(EXIT_SUCCESS);
case 'h': // help
case '?': // invalid option
case ':': // missing argument
cli_print_help();
exit((c == 'h') ? EXIT_SUCCESS : EXIT_FAILURE);
}
}
if (optind < argc) {
int i = 0;
args_info->inputs_num = (unsigned int)(argc - optind);
args_info->inputs =
(char**)(malloc ((args_info->inputs_num)*sizeof(char*)));
while (optind < argc)
if (argv[optind++] != argv[0]) {
args_info->inputs[i++] = strdup(argv[optind-1]);
}
}
return EXIT_SUCCESS;
}
FileSystem
=========
A file change watcher wrapper based on [fs](https://github.com/synrc/fs)
## System Support
- Mac fsevent
- Linux, FreeBSD and OpenBSD inotify
- Windows inotify-win
NOTE:
On Linux, FreeBSD and OpenBSD you need to install inotify-tools.
On Macos 10.14, you need run `open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg` to compile `mac_listener`.
## Usage
Put `file_system` in the `deps` and `application` part of your mix.exs
``` elixir
defmodule Excellent.Mixfile do
use Mix.Project
def project do
...
end
defp deps do
[
{ :file_system, "~> 0.2", only: :test },
]
end
...
end
```
### Subscription API
You can spawn a worker and subscribe to events from it:
```elixir
{:ok, pid} = FileSystem.start_link(dirs: ["/path/to/some/files"])
FileSystem.subscribe(pid)
```
or
```elixir
{:ok, pid} = FileSystem.start_link(dirs: ["/path/to/some/files"], name: :my_monitor_name)
FileSystem.subscribe(:my_monitor_name)
```
The pid you subscribed from will now receive messages like
```
{:file_event, worker_pid, {file_path, events}}
```
and
```
{:file_event, worker_pid, :stop}
```
### Example with GenServer
```elixir
defmodule Watcher do
use GenServer
def start_link(args) do
GenServer.start_link(__MODULE__, args)
end
def init(args) do
{:ok, watcher_pid} = FileSystem.start_link(args)
FileSystem.subscribe(watcher_pid)
{:ok, %{watcher_pid: watcher_pid}}
end
def handle_info({:file_event, watcher_pid, {path, events}}, %{watcher_pid: watcher_pid}=state) do
# YOUR OWN LOGIC FOR PATH AND EVENTS
{:noreply, state}
end
def handle_info({:file_event, watcher_pid, :stop}, %{watcher_pid: watcher_pid}=state) do
# YOUR OWN LOGIC WHEN MONITOR STOP
{:noreply, state}
end
end
```
## Tweaking behaviour via extra arguments
For each platform, you can pass extra arguments to the underlying listener process.
Each backend support different extra arguments, check backend module documentation for more information.
Here is an example to get instant notifications on file changes for Mac OS X:
```elixir
FileSystem.start_link(dirs: ["/path/to/some/files"], latency: 0, watch_root: true)
```
{application,custom_customs,
[{applications,[kernel,stdlib,elixir,logger]},
{description,"custom_customs"},
{modules,['Elixir.CustomCustoms']},
{registered,[]},
{vsn,"0.1.0"}]}.
{application,mix_test_watch,
[{applications,[kernel,stdlib,elixir,file_system]},
{description,"Automatically run tests when files change"},
{modules,['Elixir.Mix.Tasks.Test.Watch','Elixir.MixTestWatch',
'Elixir.MixTestWatch.Config',
'Elixir.MixTestWatch.MessageInbox',
'Elixir.MixTestWatch.Path',
'Elixir.MixTestWatch.PortRunner',
'Elixir.MixTestWatch.Runner',
'Elixir.MixTestWatch.Watcher']},
{registered,[]},
{vsn,"1.0.2"},
{mod,{'Elixir.MixTestWatch',[]}}]}.
{application,file_system,
[{applications,[kernel,stdlib,elixir,logger]},
{description,"A file system change watcher wrapper based on [fs](https://github.com/synrc/fs)"},
{modules,['Elixir.FileSystem','Elixir.FileSystem.Backend',
'Elixir.FileSystem.Backends.FSInotify',
'Elixir.FileSystem.Backends.FSMac',
'Elixir.FileSystem.Backends.FSPoll',
'Elixir.FileSystem.Backends.FSWindows',
'Elixir.FileSystem.Worker']},
{registered,[]},
{vsn,"0.2.10"}]}.
{application,custom_customs,
[{applications,[kernel,stdlib,elixir,logger]},
{description,"custom_customs"},
{modules,['Elixir.CustomCustoms']},
{registered,[]},
{vsn,"0.1.0"}]}.
# CustomCustoms
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `custom_customs` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:custom_customs, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/custom_customs](https://hexdocs.pm/custom_customs).