git_cl_test.py 226 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627
  1. #!/usr/bin/env vpython3
  2. # coding=utf-8
  3. # Copyright (c) 2012 The Chromium Authors. All rights reserved.
  4. # Use of this source code is governed by a BSD-style license that can be
  5. # found in the LICENSE file.
  6. """Unit tests for git_cl.py."""
  7. import codecs
  8. import datetime
  9. import json
  10. import logging
  11. import multiprocessing
  12. import optparse
  13. import os
  14. import io
  15. import shutil
  16. import sys
  17. import tempfile
  18. import unittest
  19. from unittest import mock
  20. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  21. import scm_mock
  22. import metrics
  23. import metrics_utils
  24. # We have to disable monitoring before importing git_cl.
  25. metrics_utils.COLLECT_METRICS = False
  26. import clang_format
  27. import contextlib
  28. import gclient_utils
  29. import gerrit_util
  30. import git_cl
  31. import git_common
  32. import git_footers
  33. import git_new_branch
  34. import owners_client
  35. import scm
  36. import subprocess2
  37. # TODO: Should fix these warnings.
  38. # pylint: disable=line-too-long
  39. def callError(code=1, cmd='', cwd='', stdout=b'', stderr=b''):
  40. return subprocess2.CalledProcessError(code, cmd, cwd, stdout, stderr)
  41. CERR1 = callError(1)
  42. def getAccountDetailsMock(host, account_id='self'):
  43. if account_id == 'self':
  44. return {
  45. '_account_id': 123456,
  46. 'avatars': [],
  47. 'email': 'getAccountDetailsMock@example.com',
  48. 'name': 'GetAccountDetails(self)',
  49. 'status': 'OOO',
  50. }
  51. return None
  52. class TemporaryFileMock(object):
  53. def __init__(self):
  54. self.suffix = 0
  55. @contextlib.contextmanager
  56. def __call__(self):
  57. self.suffix += 1
  58. yield '/tmp/fake-temp' + str(self.suffix)
  59. class ChangelistMock(object):
  60. # A class variable so we can access it when we don't have access to the
  61. # instance that's being set.
  62. desc = ''
  63. def __init__(self, gerrit_change=None, use_python3=False, **kwargs):
  64. self._gerrit_change = gerrit_change
  65. self._use_python3 = use_python3
  66. def GetIssue(self):
  67. return 1
  68. def FetchDescription(self):
  69. return ChangelistMock.desc
  70. def UpdateDescription(self, desc, force=False):
  71. ChangelistMock.desc = desc
  72. def GetGerritChange(self, patchset=None, **kwargs):
  73. del patchset
  74. return self._gerrit_change
  75. def GetRemoteBranch(self):
  76. return ('origin', 'refs/remotes/origin/main')
  77. class WatchlistsMock(object):
  78. def __init__(self, _):
  79. pass
  80. @staticmethod
  81. def GetWatchersForPaths(_):
  82. return ['joe@example.com']
  83. class CodereviewSettingsFileMock(object):
  84. def __init__(self):
  85. pass
  86. # pylint: disable=no-self-use
  87. def read(self):
  88. return ('CODE_REVIEW_SERVER: gerrit.chromium.org\n' +
  89. 'GERRIT_HOST: True\n')
  90. class AuthenticatorMock(object):
  91. def __init__(self, *_args):
  92. pass
  93. def has_cached_credentials(self):
  94. return True
  95. def authorize(self, http):
  96. return http
  97. def CookiesAuthenticatorMockFactory(hosts_with_creds=None, same_auth=False):
  98. """Use to mock Gerrit/Git credentials from ~/.gitcookies.
  99. Usage:
  100. >>> self.mock(git_cl.gerrit_util, "CookiesAuthenticator",
  101. CookiesAuthenticatorMockFactory({'host': ('user', 'pass')})
  102. OR
  103. >>> self.mock(git_cl.gerrit_util, "CookiesAuthenticator",
  104. CookiesAuthenticatorMockFactory(
  105. same_auth=('user', 'pass'))
  106. """
  107. class CookiesAuthenticatorMock(git_cl.gerrit_util.CookiesAuthenticator):
  108. def __init__(self): # pylint: disable=super-init-not-called
  109. # Intentionally not calling super() because it reads actual cookie
  110. # files.
  111. pass
  112. @classmethod
  113. def get_gitcookies_path(cls):
  114. return os.path.join('~', '.gitcookies')
  115. def _get_auth_for_host(self, host):
  116. if same_auth:
  117. return same_auth
  118. return (hosts_with_creds or {}).get(host)
  119. return CookiesAuthenticatorMock
  120. class MockChangelistWithBranchAndIssue():
  121. def __init__(self, branch, issue):
  122. self.branch = branch
  123. self.issue = issue
  124. def GetBranch(self):
  125. return self.branch
  126. def GetIssue(self):
  127. return self.issue
  128. class SystemExitMock(Exception):
  129. pass
  130. class ParserErrorMock(Exception):
  131. pass
  132. class TestGitClBasic(unittest.TestCase):
  133. def setUp(self):
  134. mock.patch('sys.exit', side_effect=SystemExitMock).start()
  135. mock.patch('sys.stdout', io.StringIO()).start()
  136. mock.patch('sys.stderr', io.StringIO()).start()
  137. self.addCleanup(mock.patch.stopall)
  138. def test_die_with_error(self):
  139. with self.assertRaises(SystemExitMock):
  140. git_cl.DieWithError('foo', git_cl.ChangeDescription('lorem ipsum'))
  141. self.assertEqual(sys.stderr.getvalue(), 'foo\n')
  142. self.assertTrue('saving CL description' in sys.stdout.getvalue())
  143. self.assertTrue('Content of CL description' in sys.stdout.getvalue())
  144. self.assertTrue('lorem ipsum' in sys.stdout.getvalue())
  145. sys.exit.assert_called_once_with(1)
  146. def test_die_with_error_no_desc(self):
  147. with self.assertRaises(SystemExitMock):
  148. git_cl.DieWithError('foo')
  149. self.assertEqual(sys.stderr.getvalue(), 'foo\n')
  150. self.assertEqual(sys.stdout.getvalue(), '')
  151. sys.exit.assert_called_once_with(1)
  152. def test_fetch_description(self):
  153. cl = git_cl.Changelist(issue=1, codereview_host='host')
  154. cl.description = 'x'
  155. self.assertEqual(cl.FetchDescription(), 'x')
  156. @mock.patch('git_cl.Changelist.EnsureAuthenticated')
  157. @mock.patch('git_cl.Changelist.GetStatus', lambda cl: cl.status)
  158. def test_get_cl_statuses(self, *_mocks):
  159. statuses = [
  160. 'closed', 'commit', 'dry-run', 'lgtm', 'reply', 'unsent', 'waiting'
  161. ]
  162. changes = []
  163. for status in statuses:
  164. cl = git_cl.Changelist()
  165. cl.status = status
  166. changes.append(cl)
  167. actual = set(git_cl.get_cl_statuses(changes, True))
  168. self.assertEqual(set(zip(changes, statuses)), actual)
  169. def test_upload_to_non_default_branch_no_retry(self):
  170. m = mock.patch('git_cl.Changelist._CMDUploadChange',
  171. side_effect=[git_cl.GitPushError(), None]).start()
  172. mock.patch('git_cl.Changelist.GetRemoteBranch',
  173. return_value=('foo', 'bar')).start()
  174. mock.patch('git_cl.Changelist.GetGerritProject',
  175. return_value='foo').start()
  176. mock.patch('git_cl.gerrit_util.GetProjectHead',
  177. return_value='refs/heads/main').start()
  178. cl = git_cl.Changelist()
  179. options = optparse.Values()
  180. options.target_branch = 'refs/heads/bar'
  181. with self.assertRaises(SystemExitMock):
  182. cl.CMDUploadChange(options, [], 'foo',
  183. git_cl.ChangeDescription('bar'))
  184. # ensure upload is called once
  185. self.assertEqual(len(m.mock_calls), 1)
  186. sys.exit.assert_called_once_with(1)
  187. # option not set as retry didn't happen
  188. self.assertFalse(hasattr(options, 'force'))
  189. self.assertFalse(hasattr(options, 'edit_description'))
  190. def test_upload_to_meta_config_branch_no_retry(self):
  191. m = mock.patch('git_cl.Changelist._CMDUploadChange',
  192. side_effect=[git_cl.GitPushError(), None]).start()
  193. mock.patch('git_cl.Changelist.GetRemoteBranch',
  194. return_value=('foo', 'bar')).start()
  195. mock.patch('git_cl.Changelist.GetGerritProject',
  196. return_value='foo').start()
  197. mock.patch('git_cl.gerrit_util.GetProjectHead',
  198. return_value='refs/heads/main').start()
  199. cl = git_cl.Changelist()
  200. options = optparse.Values()
  201. options.target_branch = 'refs/meta/config'
  202. with self.assertRaises(SystemExitMock):
  203. cl.CMDUploadChange(options, [], 'foo',
  204. git_cl.ChangeDescription('bar'))
  205. # ensure upload is called once
  206. self.assertEqual(len(m.mock_calls), 1)
  207. sys.exit.assert_called_once_with(1)
  208. # option not set as retry didn't happen
  209. self.assertFalse(hasattr(options, 'force'))
  210. self.assertFalse(hasattr(options, 'edit_description'))
  211. def test_upload_to_old_default_still_active(self):
  212. m = mock.patch('git_cl.Changelist._CMDUploadChange',
  213. side_effect=[git_cl.GitPushError(), None]).start()
  214. mock.patch('git_cl.Changelist.GetRemoteBranch',
  215. return_value=('foo', git_cl.DEFAULT_OLD_BRANCH)).start()
  216. mock.patch('git_cl.Changelist.GetGerritProject',
  217. return_value='foo').start()
  218. mock.patch('git_cl.gerrit_util.GetProjectHead',
  219. return_value='refs/heads/main').start()
  220. cl = git_cl.Changelist()
  221. options = optparse.Values()
  222. options.target_branch = 'refs/heads/main'
  223. with self.assertRaises(SystemExitMock):
  224. cl.CMDUploadChange(options, [], 'foo',
  225. git_cl.ChangeDescription('bar'))
  226. # ensure upload is called once
  227. self.assertEqual(len(m.mock_calls), 1)
  228. sys.exit.assert_called_once_with(1)
  229. # option not set as retry didn't happen
  230. self.assertFalse(hasattr(options, 'force'))
  231. self.assertFalse(hasattr(options, 'edit_description'))
  232. def test_upload_with_message_file_no_editor(self):
  233. m = mock.patch('git_cl.ChangeDescription.prompt',
  234. return_value=None).start()
  235. mock.patch('git_cl.Changelist.GetRemoteBranch',
  236. return_value=('foo', git_cl.DEFAULT_NEW_BRANCH)).start()
  237. mock.patch('git_cl.GetTargetRef',
  238. return_value='refs/heads/main').start()
  239. mock.patch('git_cl.Changelist._GerritCommitMsgHookCheck',
  240. lambda offer_removal: None).start()
  241. mock.patch('git_cl.Changelist.GetIssue', return_value=None).start()
  242. mock.patch('git_cl.Changelist.GetBranch',
  243. side_effect=SystemExitMock).start()
  244. mock.patch('git_cl.GenerateGerritChangeId', return_value=None).start()
  245. mock.patch('git_cl.RunGit').start()
  246. cl = git_cl.Changelist()
  247. options = optparse.Values()
  248. options.target_branch = 'refs/heads/main'
  249. options.squash = True
  250. options.edit_description = False
  251. options.force = False
  252. options.preserve_tryjobs = False
  253. options.message_file = "message.txt"
  254. options.commit_description = None
  255. with self.assertRaises(SystemExitMock):
  256. cl.CMDUploadChange(options, [], 'foo',
  257. git_cl.ChangeDescription('bar'))
  258. self.assertEqual(len(m.mock_calls), 0)
  259. options.message_file = None
  260. with self.assertRaises(SystemExitMock):
  261. cl.CMDUploadChange(options, [], 'foo',
  262. git_cl.ChangeDescription('bar'))
  263. self.assertEqual(len(m.mock_calls), 1)
  264. def test_get_cl_statuses_no_changes(self):
  265. self.assertEqual([], list(git_cl.get_cl_statuses([], True)))
  266. @mock.patch('git_cl.Changelist.EnsureAuthenticated')
  267. @mock.patch('multiprocessing.pool.ThreadPool')
  268. def test_get_cl_statuses_timeout(self, *_mocks):
  269. changes = [git_cl.Changelist() for _ in range(2)]
  270. pool = multiprocessing.pool.ThreadPool()
  271. it = pool.imap_unordered.return_value.__iter__ = mock.Mock()
  272. it.return_value.next.side_effect = [
  273. (changes[0], 'lgtm'),
  274. multiprocessing.TimeoutError,
  275. ]
  276. actual = list(git_cl.get_cl_statuses(changes, True))
  277. self.assertEqual([(changes[0], 'lgtm'), (changes[1], 'error')], actual)
  278. @mock.patch('git_cl.Changelist.GetIssueURL')
  279. def test_get_cl_statuses_not_finegrained(self, _mock):
  280. changes = [git_cl.Changelist() for _ in range(2)]
  281. urls = ['some-url', None]
  282. git_cl.Changelist.GetIssueURL.side_effect = urls
  283. actual = set(git_cl.get_cl_statuses(changes, False))
  284. self.assertEqual(set([(changes[0], 'waiting'), (changes[1], 'error')]),
  285. actual)
  286. def test_get_issue_url(self):
  287. cl = git_cl.Changelist(issue=123)
  288. cl._gerrit_server = 'https://example.com'
  289. self.assertEqual(cl.GetIssueURL(), 'https://example.com/123')
  290. self.assertEqual(cl.GetIssueURL(short=True), 'https://example.com/123')
  291. cl = git_cl.Changelist(issue=123)
  292. cl._gerrit_server = 'https://chromium-review.googlesource.com'
  293. self.assertEqual(cl.GetIssueURL(),
  294. 'https://chromium-review.googlesource.com/123')
  295. self.assertEqual(cl.GetIssueURL(short=True), 'https://crrev.com/c/123')
  296. def test_set_preserve_tryjobs(self):
  297. d = git_cl.ChangeDescription('Simple.')
  298. d.set_preserve_tryjobs()
  299. self.assertEqual(d.description.splitlines(), [
  300. 'Simple.',
  301. '',
  302. 'Cq-Do-Not-Cancel-Tryjobs: true',
  303. ])
  304. before = d.description
  305. d.set_preserve_tryjobs()
  306. self.assertEqual(before, d.description)
  307. d = git_cl.ChangeDescription('\n'.join([
  308. 'One is enough',
  309. '',
  310. 'Cq-Do-Not-Cancel-Tryjobs: dups not encouraged, but don\'t hurt',
  311. 'Change-Id: Ideadbeef',
  312. ]))
  313. d.set_preserve_tryjobs()
  314. self.assertEqual(d.description.splitlines(), [
  315. 'One is enough',
  316. '',
  317. 'Cq-Do-Not-Cancel-Tryjobs: dups not encouraged, but don\'t hurt',
  318. 'Change-Id: Ideadbeef',
  319. 'Cq-Do-Not-Cancel-Tryjobs: true',
  320. ])
  321. def test_get_bug_line_values(self):
  322. f = lambda p, bugs: list(git_cl._get_bug_line_values(p, bugs))
  323. self.assertEqual(f('', ''), [])
  324. self.assertEqual(f('', '123,v8:456'), ['123', 'v8:456'])
  325. # Prefix that ends with colon.
  326. self.assertEqual(f('v8:', '456'), ['v8:456'])
  327. self.assertEqual(f('v8:', 'chromium:123,456'),
  328. ['v8:456', 'chromium:123'])
  329. # Prefix that ends without colon.
  330. self.assertEqual(f('v8', '456'), ['v8:456'])
  331. self.assertEqual(f('v8', 'chromium:123,456'),
  332. ['v8:456', 'chromium:123'])
  333. # Not nice, but not worth carying.
  334. self.assertEqual(f('v8:', 'chromium:123,456,v8:123'),
  335. ['v8:456', 'chromium:123', 'v8:123'])
  336. self.assertEqual(f('v8', 'chromium:123,456,v8:123'),
  337. ['v8:456', 'chromium:123', 'v8:123'])
  338. @mock.patch('gerrit_util.GetAccountDetails')
  339. def test_valid_accounts(self, mockGetAccountDetails):
  340. mock_per_account = {
  341. 'u1': None, # 404, doesn't exist.
  342. 'u2': {
  343. '_account_id': 123124,
  344. 'avatars': [],
  345. 'email': 'u2@example.com',
  346. 'name': 'User Number 2',
  347. 'status': 'OOO',
  348. },
  349. 'u3': git_cl.gerrit_util.GerritError(500,
  350. 'retries didn\'t help :('),
  351. }
  352. def GetAccountDetailsMock(_, account):
  353. # Poor-man's mock library's side_effect.
  354. v = mock_per_account.pop(account)
  355. if isinstance(v, Exception):
  356. raise v
  357. return v
  358. mockGetAccountDetails.side_effect = GetAccountDetailsMock
  359. actual = git_cl.gerrit_util.ValidAccounts('host', ['u1', 'u2', 'u3'],
  360. max_threads=1)
  361. self.assertEqual(
  362. actual, {
  363. 'u2': {
  364. '_account_id': 123124,
  365. 'avatars': [],
  366. 'email': 'u2@example.com',
  367. 'name': 'User Number 2',
  368. 'status': 'OOO',
  369. },
  370. })
  371. class TestParseIssueURL(unittest.TestCase):
  372. def _test(self, arg, issue=None, patchset=None, hostname=None, fail=False):
  373. parsed = git_cl.ParseIssueNumberArgument(arg)
  374. self.assertIsNotNone(parsed)
  375. if fail:
  376. self.assertFalse(parsed.valid)
  377. return
  378. self.assertTrue(parsed.valid)
  379. self.assertEqual(parsed.issue, issue)
  380. self.assertEqual(parsed.patchset, patchset)
  381. self.assertEqual(parsed.hostname, hostname)
  382. def test_basic(self):
  383. self._test('123', 123)
  384. self._test('', fail=True)
  385. self._test('abc', fail=True)
  386. self._test('123/1', fail=True)
  387. self._test('123a', fail=True)
  388. self._test('ssh://chrome-review.source.com/#/c/123/4/', fail=True)
  389. self._test('ssh://chrome-review.source.com/c/123/1/', fail=True)
  390. def test_gerrit_url(self):
  391. self._test('https://codereview.source.com/123', 123, None,
  392. 'codereview.source.com')
  393. self._test('http://chrome-review.source.com/c/123', 123, None,
  394. 'chrome-review.source.com')
  395. self._test('https://chrome-review.source.com/c/123/', 123, None,
  396. 'chrome-review.source.com')
  397. self._test('https://chrome-review.source.com/c/123/4', 123, 4,
  398. 'chrome-review.source.com')
  399. self._test('https://chrome-review.source.com/#/c/123/4', 123, 4,
  400. 'chrome-review.source.com')
  401. self._test('https://chrome-review.source.com/c/123/4', 123, 4,
  402. 'chrome-review.source.com')
  403. self._test('https://chrome-review.source.com/123', 123, None,
  404. 'chrome-review.source.com')
  405. self._test('https://chrome-review.source.com/123/4', 123, 4,
  406. 'chrome-review.source.com')
  407. self._test('https://chrome-review.source.com/bad/123/4', fail=True)
  408. self._test('https://chrome-review.source.com/c/123/1/whatisthis',
  409. fail=True)
  410. self._test('https://chrome-review.source.com/c/abc/', fail=True)
  411. def test_short_urls(self):
  412. self._test('https://crrev.com/c/2151934', 2151934, None,
  413. 'chromium-review.googlesource.com')
  414. def test_missing_scheme(self):
  415. self._test('codereview.source.com/123', 123, None,
  416. 'codereview.source.com')
  417. self._test('crrev.com/c/2151934', 2151934, None,
  418. 'chromium-review.googlesource.com')
  419. class GitCookiesCheckerTest(unittest.TestCase):
  420. def setUp(self):
  421. super(GitCookiesCheckerTest, self).setUp()
  422. self.c = git_cl._GitCookiesChecker()
  423. self.c._all_hosts = []
  424. mock.patch('sys.stdout', io.StringIO()).start()
  425. self.addCleanup(mock.patch.stopall)
  426. def mock_hosts_creds(self, subhost_identity_pairs):
  427. def ensure_googlesource(h):
  428. if not h.endswith(git_cl._GOOGLESOURCE):
  429. assert not h.endswith('.')
  430. return h + '.' + git_cl._GOOGLESOURCE
  431. return h
  432. self.c._all_hosts = [(ensure_googlesource(h), i, '.gitcookies')
  433. for h, i in subhost_identity_pairs]
  434. def test_identity_parsing(self):
  435. self.assertEqual(self.c._parse_identity('ldap.google.com'),
  436. ('ldap', 'google.com'))
  437. self.assertEqual(self.c._parse_identity('git-ldap.example.com'),
  438. ('ldap', 'example.com'))
  439. # Specical case because we know there are no subdomains in chromium.org.
  440. self.assertEqual(self.c._parse_identity('git-note.period.chromium.org'),
  441. ('note.period', 'chromium.org'))
  442. # Pathological: ".period." can be either username OR domain, more likely
  443. # domain.
  444. self.assertEqual(self.c._parse_identity('git-note.period.example.com'),
  445. ('note', 'period.example.com'))
  446. def test_analysis_nothing(self):
  447. self.c._all_hosts = []
  448. self.assertFalse(self.c.has_generic_host())
  449. self.assertEqual(set(), self.c.get_conflicting_hosts())
  450. self.assertEqual(set(), self.c.get_duplicated_hosts())
  451. self.assertEqual(set(), self.c.get_partially_configured_hosts())
  452. def test_analysis(self):
  453. self.mock_hosts_creds([
  454. ('.googlesource.com', 'git-example.chromium.org'),
  455. ('chromium', 'git-example.google.com'),
  456. ('chromium-review', 'git-example.google.com'),
  457. ('chrome-internal', 'git-example.chromium.org'),
  458. ('chrome-internal-review', 'git-example.chromium.org'),
  459. ('conflict', 'git-example.google.com'),
  460. ('conflict-review', 'git-example.chromium.org'),
  461. ('dup', 'git-example.google.com'),
  462. ('dup', 'git-example.google.com'),
  463. ('dup-review', 'git-example.google.com'),
  464. ('partial', 'git-example.google.com'),
  465. ('gpartial-review', 'git-example.google.com'),
  466. ])
  467. self.assertTrue(self.c.has_generic_host())
  468. self.assertEqual(set(['conflict.googlesource.com']),
  469. self.c.get_conflicting_hosts())
  470. self.assertEqual(set(['dup.googlesource.com']),
  471. self.c.get_duplicated_hosts())
  472. self.assertEqual(
  473. set([
  474. 'partial.googlesource.com', 'gpartial-review.googlesource.com'
  475. ]), self.c.get_partially_configured_hosts())
  476. def test_report_no_problems(self):
  477. self.test_analysis_nothing()
  478. self.assertFalse(self.c.find_and_report_problems())
  479. self.assertEqual(sys.stdout.getvalue(), '')
  480. @mock.patch('git_cl.gerrit_util.CookiesAuthenticator.get_gitcookies_path',
  481. return_value=os.path.join('~', '.gitcookies'))
  482. def test_report(self, *_mocks):
  483. self.test_analysis()
  484. self.assertTrue(self.c.find_and_report_problems())
  485. with open(
  486. os.path.join(os.path.dirname(__file__),
  487. 'git_cl_creds_check_report.txt')) as f:
  488. expected = f.read() % {
  489. 'sep': os.sep,
  490. }
  491. def by_line(text):
  492. return [l.rstrip() for l in text.rstrip().splitlines()]
  493. self.maxDiff = 10000 # pylint: disable=attribute-defined-outside-init
  494. self.assertEqual(by_line(sys.stdout.getvalue().strip()),
  495. by_line(expected))
  496. class TestGitCl(unittest.TestCase):
  497. def setUp(self):
  498. super(TestGitCl, self).setUp()
  499. self.calls = []
  500. self._calls_done = []
  501. oldEnv = dict(os.environ)
  502. def _resetEnv():
  503. os.environ = oldEnv
  504. self.addCleanup(_resetEnv)
  505. self.failed = False
  506. mock.patch('sys.stdout', io.StringIO()).start()
  507. mock.patch('git_cl.time_time',
  508. lambda: self._mocked_call('time.time')).start()
  509. mock.patch('git_cl.metrics.collector.add_repeated',
  510. lambda *a: self._mocked_call('add_repeated', *a)).start()
  511. mock.patch('subprocess2.call', self._mocked_call).start()
  512. mock.patch('subprocess2.check_call', self._mocked_call).start()
  513. mock.patch('subprocess2.check_output', self._mocked_call).start()
  514. mock.patch('subprocess2.communicate', lambda *a, **_k:
  515. ([self._mocked_call(*a), ''], 0)).start()
  516. mock.patch('git_cl.gclient_utils.CheckCallAndFilter',
  517. self._mocked_call).start()
  518. mock.patch('git_common.is_dirty_git_tree', lambda x: False).start()
  519. mock.patch('git_cl.FindCodereviewSettingsFile', return_value='').start()
  520. mock.patch(
  521. 'git_cl.SaveDescriptionBackup',
  522. lambda _: self._mocked_call('SaveDescriptionBackup')).start()
  523. mock.patch('git_cl.write_json',
  524. lambda *a: self._mocked_call('write_json', *a)).start()
  525. mock.patch('git_cl.Changelist.RunHook',
  526. return_value={
  527. 'more_cc': ['test-more-cc@chromium.org']
  528. }).start()
  529. mock.patch('git_cl.watchlists.Watchlists', WatchlistsMock).start()
  530. mock.patch('git_cl.auth.Authenticator', AuthenticatorMock).start()
  531. mock.patch('gerrit_util.GetChangeDetail').start()
  532. mock.patch(
  533. 'git_cl.gerrit_util.GetChangeComments',
  534. lambda *a: self._mocked_call('GetChangeComments', *a)).start()
  535. mock.patch(
  536. 'git_cl.gerrit_util.GetChangeRobotComments',
  537. lambda *a: self._mocked_call('GetChangeRobotComments', *a)).start()
  538. mock.patch('git_cl.gerrit_util.AddReviewers',
  539. lambda *a: self._mocked_call('AddReviewers', *a)).start()
  540. mock.patch('git_cl.gerrit_util.SetReview',
  541. lambda h, i, msg=None, labels=None, notify=None, ready=None:
  542. (self._mocked_call('SetReview', h, i, msg, labels, notify,
  543. ready))).start()
  544. mock.patch('git_cl.gerrit_util.LuciContextAuthenticator.is_applicable',
  545. return_value=False).start()
  546. mock.patch('git_cl.gerrit_util.GceAuthenticator.is_applicable',
  547. return_value=False).start()
  548. mock.patch('git_cl.gerrit_util.ValidAccounts',
  549. lambda *a: self._mocked_call('ValidAccounts', *a)).start()
  550. mock.patch('sys.exit', side_effect=SystemExitMock).start()
  551. mock.patch('git_cl.Settings.GetRoot', return_value='').start()
  552. scm_mock.GIT(self)
  553. mock.patch('scm.GIT.ResolveCommit', return_value='hash').start()
  554. mock.patch('scm.GIT.IsValidRevision', return_value=True).start()
  555. mock.patch('scm.GIT.FetchUpstreamTuple',
  556. return_value=('origin', 'refs/heads/main')).start()
  557. mock.patch('scm.GIT.CaptureStatus',
  558. return_value=[('M', 'foo.txt')]).start()
  559. # It's important to reset settings to not have inter-tests interference.
  560. git_cl.settings = git_cl.Settings()
  561. self.addCleanup(mock.patch.stopall)
  562. gerrit_util._Authenticator._resolved = None
  563. def tearDown(self):
  564. try:
  565. if not self.failed:
  566. self.assertEqual([], self.calls)
  567. except AssertionError:
  568. calls = ''.join(' %s\n' % str(call) for call in self.calls[:5])
  569. if len(self.calls) > 5:
  570. calls += ' ...\n'
  571. self.fail(
  572. '\n'
  573. 'There are un-consumed calls after this test has finished:\n' +
  574. calls)
  575. finally:
  576. super(TestGitCl, self).tearDown()
  577. def _mocked_call(self, *args, **_kwargs):
  578. self.assertTrue(
  579. self.calls, '@%d Expected: <Missing> Actual: %r' %
  580. (len(self._calls_done), args))
  581. top = self.calls.pop(0)
  582. expected_args, result = top
  583. # Also logs otherwise it could get caught in a try/finally and be hard
  584. # to diagnose.
  585. if expected_args != args:
  586. N = 5
  587. prior_calls = '\n '.join(
  588. '@%d: %r' % (len(self._calls_done) - N + i, c[0])
  589. for i, c in enumerate(self._calls_done[-N:]))
  590. following_calls = '\n '.join('@%d: %r' %
  591. (len(self._calls_done) + i + 1, c[0])
  592. for i, c in enumerate(self.calls[:N]))
  593. extended_msg = ('A few prior calls:\n %s\n\n'
  594. 'This (expected):\n @%d: %r\n'
  595. 'This (actual):\n @%d: %r\n\n'
  596. 'A few following expected calls:\n %s' %
  597. (prior_calls, len(self._calls_done), expected_args,
  598. len(self._calls_done), args, following_calls))
  599. self.failed = True
  600. self.fail(
  601. '@%d\n'
  602. ' Expected: %r\n'
  603. ' Actual: %r\n'
  604. '\n'
  605. '%s' %
  606. (len(self._calls_done), expected_args, args, extended_msg))
  607. self._calls_done.append(top)
  608. if isinstance(result, Exception):
  609. raise result
  610. # stdout from git commands is supposed to be a bytestream. Convert it
  611. # here instead of converting all test output in this file to bytes.
  612. if args[0][0] == 'git' and not isinstance(result, bytes):
  613. result = result.encode('utf-8')
  614. return result
  615. @mock.patch('sys.stdin', io.StringIO('blah\nye\n'))
  616. @mock.patch('sys.stdout', io.StringIO())
  617. def test_ask_for_explicit_yes_true(self):
  618. self.assertTrue(git_cl.ask_for_explicit_yes('prompt'))
  619. self.assertEqual('prompt [Yes/No]: Please, type yes or no: ',
  620. sys.stdout.getvalue())
  621. def test_LoadCodereviewSettingsFromFile_gerrit(self):
  622. codereview_file = io.StringIO('GERRIT_HOST: true')
  623. self.calls = [
  624. ]
  625. self.assertIsNone(
  626. git_cl.LoadCodereviewSettingsFromFile(codereview_file))
  627. @classmethod
  628. def _gerrit_base_calls(cls,
  629. issue=None,
  630. fetched_description=None,
  631. fetched_status=None,
  632. other_cl_owner=None,
  633. custom_cl_base=None,
  634. short_hostname='chromium',
  635. change_id=None,
  636. default_branch='main',
  637. reset_issue=False):
  638. calls = [
  639. (
  640. (['os.path.isfile', '.gitmodules'], ),
  641. 'True',
  642. ),
  643. ]
  644. if custom_cl_base:
  645. ancestor_revision = custom_cl_base
  646. else:
  647. # Determine ancestor_revision to be merge base.
  648. ancestor_revision = 'origin/' + default_branch
  649. if issue:
  650. # TODO: if tests don't provide a `change_id` the default used here
  651. # will cause the TRACES_README_FORMAT mock (which uses the test
  652. # provided `change_id` to fail.
  653. gerrit_util.GetChangeDetail.return_value = {
  654. 'owner': {
  655. 'email': (other_cl_owner or 'owner@example.com')
  656. },
  657. 'change_id': (change_id or '123456789'),
  658. 'current_revision': 'sha1_of_current_revision',
  659. 'revisions': {
  660. 'sha1_of_current_revision': {
  661. 'commit': {
  662. 'message': fetched_description
  663. },
  664. }
  665. },
  666. 'status': fetched_status or 'NEW',
  667. }
  668. if fetched_status == 'ABANDONED':
  669. return calls
  670. if fetched_status == 'MERGED':
  671. calls.append(((
  672. 'ask_for_data',
  673. 'Change https://chromium-review.googlesource.com/%s has been '
  674. 'submitted, new uploads are not allowed. Would you like to start '
  675. 'a new change (Y/n)?' % issue),
  676. 'y' if reset_issue else 'n'))
  677. if not reset_issue:
  678. return calls
  679. # Part of SetIssue call.
  680. calls.append(((['git', 'log', '-1', '--format=%B'], ), ''))
  681. if other_cl_owner:
  682. calls += [
  683. (('ask_for_data',
  684. 'Press Enter to upload, or Ctrl+C to abort'), ''),
  685. ]
  686. calls += [
  687. ((['git', 'rev-list', '--count'] +
  688. ([f'{custom_cl_base}..HEAD']
  689. if custom_cl_base else [f'{ancestor_revision}..HEAD']), ), '3'),
  690. ]
  691. calls += [
  692. ((['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] +
  693. ([custom_cl_base]
  694. if custom_cl_base else [ancestor_revision, 'HEAD']), ), '+dat'),
  695. ]
  696. return calls
  697. def _gerrit_upload_calls(self,
  698. description,
  699. reviewers,
  700. squash,
  701. squash_mode='default',
  702. title=None,
  703. notify=False,
  704. post_amend_description=None,
  705. issue=None,
  706. cc=None,
  707. custom_cl_base=None,
  708. short_hostname='chromium',
  709. labels=None,
  710. change_id=None,
  711. final_description=None,
  712. gitcookies_exists=True,
  713. force=False,
  714. edit_description=None,
  715. default_branch='main',
  716. ref_to_push='abcdef0123456789',
  717. external_parent=None,
  718. push_opts=None):
  719. if post_amend_description is None:
  720. post_amend_description = description
  721. cc = cc or []
  722. calls = []
  723. if squash_mode in ('override_squash', 'override_nosquash'):
  724. scm.GIT.SetConfig(
  725. '', 'gerrit.override-squash-uploads',
  726. 'true' if squash_mode == 'override_squash' else 'false')
  727. if not git_footers.get_footer_change_id(description) and not squash:
  728. calls += [
  729. (('DownloadGerritHook', False), ''),
  730. ]
  731. if squash:
  732. if not issue and not force:
  733. calls += [
  734. ((['RunEditor'], ), description),
  735. ]
  736. # user wants to edit description
  737. if edit_description:
  738. calls += [
  739. ((['RunEditor'], ), edit_description),
  740. ]
  741. if external_parent:
  742. parent = external_parent
  743. else:
  744. if custom_cl_base is None:
  745. parent = 'origin/' + default_branch
  746. git_common.get_or_create_merge_base.return_value = parent
  747. else:
  748. calls += [
  749. (([
  750. 'git', 'merge-base', '--is-ancestor',
  751. custom_cl_base,
  752. 'refs/remotes/origin/' + default_branch
  753. ], ), callError(1)), # Means not ancenstor.
  754. (('ask_for_data',
  755. 'Do you take responsibility for cleaning up potential mess '
  756. 'resulting from proceeding with upload? Press Enter to upload, '
  757. 'or Ctrl+C to abort'), ''),
  758. ]
  759. parent = custom_cl_base
  760. calls += [
  761. (
  762. (['git', 'rev-parse',
  763. 'HEAD:'], ), # `HEAD:` means HEAD's tree hash.
  764. '0123456789abcdef'),
  765. ((['FileWrite', '/tmp/fake-temp1', description], ), None),
  766. (([
  767. 'git', 'commit-tree', '0123456789abcdef', '-p', parent,
  768. '-F', '/tmp/fake-temp1'
  769. ], ), ref_to_push),
  770. ]
  771. else:
  772. ref_to_push = 'HEAD'
  773. parent = 'origin/refs/heads/' + default_branch
  774. calls += [
  775. (('SaveDescriptionBackup', ), None),
  776. ((['git', 'rev-list',
  777. parent + '..' + ref_to_push], ), '1hashPerLine\n'),
  778. ]
  779. metrics_arguments = []
  780. ref_suffix_list = []
  781. if notify:
  782. ref_suffix_list.append('ready,notify=ALL')
  783. metrics_arguments += ['ready', 'notify=ALL']
  784. elif not issue and squash:
  785. ref_suffix_list.append('wip')
  786. metrics_arguments.append('wip')
  787. # If issue is given, then description is fetched from Gerrit instead.
  788. if not title and squash_mode != "override_nosquash":
  789. if issue is None:
  790. if squash:
  791. title = 'Initial upload'
  792. else:
  793. calls += [
  794. ((['git', 'show', '-s', '--format=%s', 'HEAD',
  795. '--'], ), ''),
  796. (('ask_for_data', 'Title for patchset []: '), 'User input'),
  797. ]
  798. title = 'User input'
  799. if title:
  800. ref_suffix_list.append('m=' +
  801. gerrit_util.PercentEncodeForGitRef(title))
  802. metrics_arguments.append('m')
  803. for k, v in sorted((labels or {}).items()):
  804. ref_suffix_list.append('l=%s+%d' % (k, v))
  805. metrics_arguments.append('l=%s+%d' % (k, v))
  806. if short_hostname == 'chromium':
  807. # All reviewers and ccs get into ref_suffix.
  808. for r in sorted(reviewers):
  809. ref_suffix_list.append('r=%s' % r)
  810. metrics_arguments.append('r')
  811. if issue is None:
  812. cc += ['test-more-cc@chromium.org', 'joe@example.com']
  813. for c in sorted(cc):
  814. ref_suffix_list.append('cc=%s' % c)
  815. metrics_arguments.append('cc')
  816. reviewers, cc = [], []
  817. else:
  818. # TODO(crbug/877717): remove this case.
  819. calls += [(('ValidAccounts',
  820. '%s-review.googlesource.com' % short_hostname,
  821. sorted(reviewers) +
  822. ['joe@example.com', 'test-more-cc@chromium.org'] + cc),
  823. {
  824. e: {
  825. 'email': e
  826. }
  827. for e in (reviewers + ['joe@example.com'] + cc)
  828. })]
  829. for r in sorted(reviewers):
  830. if r != 'bad-account-or-email':
  831. ref_suffix_list.append('r=%s' % r)
  832. metrics_arguments.append('r')
  833. reviewers.remove(r)
  834. if issue is None:
  835. cc += ['joe@example.com']
  836. for c in sorted(cc):
  837. ref_suffix_list.append('cc=%s' % c)
  838. metrics_arguments.append('cc')
  839. if c in cc:
  840. cc.remove(c)
  841. ref_suffix = ''
  842. if ref_suffix_list:
  843. ref_suffix = '%' + ','.join(ref_suffix_list)
  844. calls += [
  845. (
  846. ('time.time', ),
  847. 1000,
  848. ),
  849. (
  850. ([
  851. 'git', 'push',
  852. 'https://%s.googlesource.com/my/repo' % short_hostname,
  853. ref_to_push + ':refs/for/refs/heads/' + default_branch +
  854. ref_suffix
  855. ] + (push_opts if push_opts else []), ),
  856. (('remote:\n'
  857. 'remote: Processing changes: (\)\n'
  858. 'remote: Processing changes: (|)\n'
  859. 'remote: Processing changes: (/)\n'
  860. 'remote: Processing changes: (-)\n'
  861. 'remote: Processing changes: new: 1 (/)\n'
  862. 'remote: Processing changes: new: 1, done\n'
  863. 'remote:\n'
  864. 'remote: New Changes:\n'
  865. 'remote: '
  866. 'https://%s-review.googlesource.com/#/c/my/repo/+/123456'
  867. ' XXX\n'
  868. 'remote:\n'
  869. 'To https://%s.googlesource.com/my/repo\n'
  870. ' * [new branch] hhhh -> refs/for/refs/heads/%s\n') %
  871. (short_hostname, short_hostname, default_branch)),
  872. ),
  873. (
  874. ('time.time', ),
  875. 2000,
  876. ),
  877. (
  878. ('add_repeated', 'sub_commands', {
  879. 'execution_time': 1000,
  880. 'command': 'git push',
  881. 'exit_code': 0,
  882. 'arguments': sorted(metrics_arguments),
  883. }),
  884. None,
  885. ),
  886. ]
  887. final_description = final_description or post_amend_description.strip()
  888. trace_name = os.path.join('TRACES_DIR', '20170316T200041.000000')
  889. # Trace-related calls
  890. calls += [
  891. # Write a description with context for the current trace.
  892. (
  893. ([
  894. 'FileWrite', trace_name + '-README',
  895. '%(date)s\n'
  896. '%(short_hostname)s-review.googlesource.com\n'
  897. '%(change_id)s\n'
  898. '%(title)s\n'
  899. '%(description)s\n'
  900. '1000\n'
  901. '0\n'
  902. '%(trace_name)s' % {
  903. 'date': '2017-03-16T20:00:41.000000',
  904. 'short_hostname': short_hostname,
  905. 'change_id': change_id,
  906. 'description': final_description,
  907. 'title': title or '<untitled>',
  908. 'trace_name': trace_name,
  909. }
  910. ], ),
  911. None,
  912. ),
  913. # Read traces and shorten git hashes.
  914. (
  915. (['os.path.isfile',
  916. os.path.join('TEMP_DIR', 'trace-packet')], ),
  917. True,
  918. ),
  919. (
  920. (['FileRead',
  921. os.path.join('TEMP_DIR', 'trace-packet')], ),
  922. ('git-hash: 0123456789012345678901234567890123456789\n'
  923. 'git-hash: abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde\n'),
  924. ),
  925. (
  926. ([
  927. 'FileWrite',
  928. os.path.join('TEMP_DIR', 'trace-packet'),
  929. 'git-hash: 012345\n'
  930. 'git-hash: abcdea\n'
  931. ], ),
  932. None,
  933. ),
  934. # Make zip file for the git traces.
  935. (
  936. (['make_archive', trace_name + '-traces', 'zip', 'TEMP_DIR'], ),
  937. None,
  938. ),
  939. # Collect git config and gitcookies.
  940. #
  941. # We accept ANY for the git-config file because it's just reflecting
  942. # our mocked git config in scm.GIT anyway.
  943. (
  944. ([
  945. 'FileWrite',
  946. os.path.join('TEMP_DIR', 'git-config'), mock.ANY,
  947. ], ),
  948. None,
  949. ),
  950. (
  951. (['os.path.isfile',
  952. os.path.join('~', '.gitcookies')], ),
  953. gitcookies_exists,
  954. ),
  955. ]
  956. if gitcookies_exists:
  957. calls += [
  958. (
  959. (['FileRead', os.path.join('~', '.gitcookies')], ),
  960. 'gitcookies 1/SECRET',
  961. ),
  962. (
  963. ([
  964. 'FileWrite',
  965. os.path.join(
  966. 'TEMP_DIR',
  967. 'CookiesAuthenticatorMock.debug_summary_state'),
  968. 'gitcookies REDACTED'
  969. ], ),
  970. None,
  971. ),
  972. ]
  973. else:
  974. calls += [
  975. (
  976. ([
  977. 'FileWrite',
  978. os.path.join(
  979. 'TEMP_DIR',
  980. 'CookiesAuthenticatorMock.debug_summary_state'), ''
  981. ], ),
  982. None,
  983. ),
  984. ]
  985. calls += [
  986. # Make zip file for the git config and gitcookies.
  987. (
  988. (['make_archive', trace_name + '-git-info', 'zip',
  989. 'TEMP_DIR'], ),
  990. None,
  991. ),
  992. ]
  993. # TODO(crbug/877717): this should never be used.
  994. if squash and short_hostname != 'chromium':
  995. calls += [
  996. (('AddReviewers', 'chromium-review.googlesource.com',
  997. 'my%2Frepo~123456', sorted(reviewers),
  998. cc + ['test-more-cc@chromium.org'], notify), ''),
  999. ]
  1000. return calls
  1001. def _run_gerrit_upload_test(self,
  1002. upload_args,
  1003. description,
  1004. reviewers=None,
  1005. squash=True,
  1006. squash_mode=None,
  1007. title=None,
  1008. notify=False,
  1009. post_amend_description=None,
  1010. issue=None,
  1011. patchset=None,
  1012. cc=None,
  1013. fetched_status=None,
  1014. other_cl_owner=None,
  1015. custom_cl_base=None,
  1016. short_hostname='chromium',
  1017. labels=None,
  1018. change_id=None,
  1019. final_description=None,
  1020. gitcookies_exists=True,
  1021. force=False,
  1022. log_description=None,
  1023. edit_description=None,
  1024. fetched_description=None,
  1025. default_branch='main',
  1026. ref_to_push='abcdef0123456789',
  1027. external_parent=None,
  1028. push_opts=None,
  1029. reset_issue=False):
  1030. """Generic gerrit upload test framework."""
  1031. if squash_mode is None:
  1032. if '--no-squash' in upload_args:
  1033. squash_mode = 'nosquash'
  1034. elif '--squash' in upload_args:
  1035. squash_mode = 'squash'
  1036. else:
  1037. squash_mode = 'default'
  1038. reviewers = reviewers or []
  1039. cc = cc or []
  1040. mock.patch(
  1041. 'git_cl.gerrit_util.CookiesAuthenticator',
  1042. CookiesAuthenticatorMockFactory(same_auth=('git-owner.example.com',
  1043. 'pass'))).start()
  1044. mock.patch('git_cl.Changelist._GerritCommitMsgHookCheck',
  1045. lambda offer_removal: None).start()
  1046. mock.patch('git_cl.Changelist.GetMostRecentPatchset',
  1047. lambda _, update: patchset).start()
  1048. mock.patch('git_cl.gclient_utils.RunEditor',
  1049. lambda *_, **__: self._mocked_call(['RunEditor'])).start()
  1050. mock.patch('git_cl.DownloadGerritHook', lambda force: self._mocked_call(
  1051. 'DownloadGerritHook', force)).start()
  1052. mock.patch('git_cl.gclient_utils.FileRead',
  1053. lambda path: self._mocked_call(['FileRead', path])).start()
  1054. mock.patch(
  1055. 'git_cl.gclient_utils.FileWrite', lambda path, contents: self.
  1056. _mocked_call(['FileWrite', path, contents])).start()
  1057. mock.patch(
  1058. 'git_cl.datetime_now',
  1059. lambda: datetime.datetime(2017, 3, 16, 20, 0, 41, 0)).start()
  1060. mock.patch('git_cl.tempfile.mkdtemp', lambda: 'TEMP_DIR').start()
  1061. mock.patch('git_cl.TRACES_DIR', 'TRACES_DIR').start()
  1062. mock.patch(
  1063. 'git_cl.TRACES_README_FORMAT', '%(now)s\n'
  1064. '%(gerrit_host)s\n'
  1065. '%(change_id)s\n'
  1066. '%(title)s\n'
  1067. '%(description)s\n'
  1068. '%(execution_time)s\n'
  1069. '%(exit_code)s\n'
  1070. '%(trace_name)s').start()
  1071. mock.patch(
  1072. 'git_cl.shutil.make_archive', lambda *args: self._mocked_call(
  1073. ['make_archive'] + list(args))).start()
  1074. mock.patch(
  1075. 'os.path.isfile',
  1076. lambda path: self._mocked_call(['os.path.isfile', path])).start()
  1077. mock.patch('git_cl._create_description_from_log',
  1078. return_value=log_description or description).start()
  1079. mock.patch('git_cl.Changelist._AddChangeIdToCommitMessage',
  1080. return_value=post_amend_description or description).start()
  1081. mock.patch('git_cl.GenerateGerritChangeId',
  1082. return_value=change_id).start()
  1083. mock.patch('git_common.get_or_create_merge_base',
  1084. return_value='origin/' + default_branch).start()
  1085. mock.patch('gerrit_util.GetAccountDetails',
  1086. getAccountDetailsMock).start()
  1087. mock.patch(
  1088. 'gclient_utils.AskForData',
  1089. lambda prompt: self._mocked_call('ask_for_data', prompt)).start()
  1090. scm.GIT.SetConfig('', 'gerrit.host', 'true')
  1091. scm.GIT.SetConfig('', 'branch.main.gerritissue',
  1092. (str(issue) if issue else None))
  1093. scm.GIT.SetConfig('', 'remote.origin.url',
  1094. f'https://{short_hostname}.googlesource.com/my/repo')
  1095. scm.GIT.SetConfig('', 'user.email', 'owner@example.com')
  1096. if squash_mode == "override_nosquash":
  1097. if issue:
  1098. mock.patch('gerrit_util.GetChange',
  1099. return_value={
  1100. '_number': issue
  1101. }).start()
  1102. else:
  1103. mock.patch('gerrit_util.GetChange', return_value={}).start()
  1104. self.calls = self._gerrit_base_calls(
  1105. issue=issue,
  1106. fetched_description=fetched_description or description,
  1107. fetched_status=fetched_status,
  1108. other_cl_owner=other_cl_owner,
  1109. custom_cl_base=custom_cl_base,
  1110. short_hostname=short_hostname,
  1111. change_id=change_id,
  1112. default_branch=default_branch,
  1113. reset_issue=reset_issue)
  1114. if fetched_status == 'ABANDONED' or (fetched_status == 'MERGED'
  1115. and not reset_issue):
  1116. pass # readability
  1117. else:
  1118. if fetched_status == 'MERGED' and reset_issue:
  1119. fetched_status = 'NEW'
  1120. issue = None
  1121. mock.patch('gclient_utils.temporary_file',
  1122. TemporaryFileMock()).start()
  1123. mock.patch('os.remove', return_value=True).start()
  1124. self.calls += self._gerrit_upload_calls(
  1125. description,
  1126. reviewers,
  1127. squash,
  1128. squash_mode=squash_mode,
  1129. title=title,
  1130. notify=notify,
  1131. post_amend_description=post_amend_description,
  1132. issue=issue,
  1133. cc=cc,
  1134. custom_cl_base=custom_cl_base,
  1135. short_hostname=short_hostname,
  1136. labels=labels,
  1137. change_id=change_id,
  1138. final_description=final_description,
  1139. gitcookies_exists=gitcookies_exists,
  1140. force=force,
  1141. edit_description=edit_description,
  1142. default_branch=default_branch,
  1143. ref_to_push=ref_to_push,
  1144. external_parent=external_parent,
  1145. push_opts=push_opts)
  1146. # Uncomment when debugging.
  1147. # print('\n'.join(map(lambda x: '%2i: %s' % x, enumerate(self.calls))))
  1148. git_cl.main(['upload'] + upload_args)
  1149. if squash:
  1150. self.assertIssueAndPatchset(patchset=str((patchset or 0) + 1))
  1151. self.assertEqual(
  1152. ref_to_push,
  1153. scm.GIT.GetBranchConfig('', 'main',
  1154. git_cl.GERRIT_SQUASH_HASH_CONFIG_KEY))
  1155. @unittest.skipIf(gclient_utils.IsEnvCog(),
  1156. 'not supported in non-git environment')
  1157. def test_gerrit_upload_traces_no_gitcookies(self):
  1158. self._run_gerrit_upload_test(
  1159. ['--no-squash'],
  1160. 'desc ✔\n\nBUG=\n', [],
  1161. squash=False,
  1162. post_amend_description='desc ✔\n\nBUG=\n\nChange-Id: Ixxx',
  1163. change_id='Ixxx',
  1164. gitcookies_exists=False)
  1165. @unittest.skipIf(gclient_utils.IsEnvCog(),
  1166. 'not supported in non-git environment')
  1167. def test_gerrit_upload_without_change_id_nosquash(self):
  1168. self._run_gerrit_upload_test(
  1169. ['--no-squash'],
  1170. 'desc ✔\n\nBUG=\n', [],
  1171. squash=False,
  1172. post_amend_description='desc ✔\n\nBUG=\n\nChange-Id: Ixxx',
  1173. change_id='Ixxx')
  1174. @unittest.skipIf(gclient_utils.IsEnvCog(),
  1175. 'not supported in non-git environment')
  1176. def test_gerrit_upload_without_change_id_override_nosquash(self):
  1177. self._run_gerrit_upload_test(
  1178. [],
  1179. 'desc ✔\n\nBUG=\n', [],
  1180. squash=False,
  1181. squash_mode='override_nosquash',
  1182. post_amend_description='desc ✔\n\nBUG=\n\nChange-Id: Ixxx',
  1183. change_id='Ixxx')
  1184. @unittest.skipIf(gclient_utils.IsEnvCog(),
  1185. 'not supported in non-git environment')
  1186. def test_gerrit_no_reviewer(self):
  1187. self._run_gerrit_upload_test(
  1188. [],
  1189. 'desc ✔\n\nBUG=\n\nChange-Id: I123456789\n', [],
  1190. squash=False,
  1191. squash_mode='override_nosquash',
  1192. change_id='I123456789')
  1193. @unittest.skipIf(gclient_utils.IsEnvCog(),
  1194. 'not supported in non-git environment')
  1195. def test_gerrit_push_opts(self):
  1196. self._run_gerrit_upload_test(
  1197. ['-o', 'wip'],
  1198. 'desc ✔\n\nBUG=\n\nChange-Id: I123456789\n', [],
  1199. squash=False,
  1200. squash_mode='override_nosquash',
  1201. change_id='I123456789',
  1202. push_opts=['-o', 'wip'])
  1203. @unittest.skipIf(gclient_utils.IsEnvCog(),
  1204. 'not supported in non-git environment')
  1205. def test_gerrit_no_reviewer_non_chromium_host(self):
  1206. # TODO(crbug/877717): remove this test case.
  1207. self._run_gerrit_upload_test(
  1208. [],
  1209. 'desc ✔\n\nBUG=\n\nChange-Id: I123456789\n', [],
  1210. squash=False,
  1211. squash_mode='override_nosquash',
  1212. short_hostname='other',
  1213. change_id='I123456789')
  1214. @unittest.skipIf(gclient_utils.IsEnvCog(),
  1215. 'not supported in non-git environment')
  1216. def test_gerrit_patchset_title_special_chars_nosquash(self):
  1217. self._run_gerrit_upload_test(
  1218. ['-f', '-t', 'We\'ll escape ^_ ^ special chars...@{u}'],
  1219. 'desc ✔\n\nBUG=\n\nChange-Id: I123456789',
  1220. squash=False,
  1221. squash_mode='override_nosquash',
  1222. change_id='I123456789',
  1223. title='We\'ll escape ^_ ^ special chars...@{u}')
  1224. @unittest.skipIf(gclient_utils.IsEnvCog(),
  1225. 'not supported in non-git environment')
  1226. def test_gerrit_reviewers_cmd_line(self):
  1227. self._run_gerrit_upload_test(
  1228. ['-r', 'foo@example.com', '--send-mail'],
  1229. 'desc ✔\n\nBUG=\n\nChange-Id: I123456789',
  1230. reviewers=['foo@example.com'],
  1231. squash=False,
  1232. squash_mode='override_nosquash',
  1233. notify=True,
  1234. change_id='I123456789',
  1235. final_description=(
  1236. 'desc ✔\n\nBUG=\nR=foo@example.com\n\nChange-Id: I123456789'))
  1237. @unittest.skipIf(gclient_utils.IsEnvCog(),
  1238. 'not supported in non-git environment')
  1239. def test_gerrit_reviewers_cmd_line_send_email(self):
  1240. self._run_gerrit_upload_test(
  1241. ['-r', 'foo@example.com', '--send-email'],
  1242. 'desc ✔\n\nBUG=\n\nChange-Id: I123456789',
  1243. reviewers=['foo@example.com'],
  1244. squash=False,
  1245. squash_mode='override_nosquash',
  1246. notify=True,
  1247. change_id='I123456789',
  1248. final_description=(
  1249. 'desc ✔\n\nBUG=\nR=foo@example.com\n\nChange-Id: I123456789'))
  1250. @mock.patch('git_cl.Changelist.GetGerritHost',
  1251. return_value='chromium-review.googlesource.com')
  1252. @mock.patch('git_cl.Changelist.GetRemoteBranch',
  1253. return_value=('origin', 'refs/remotes/origin/main'))
  1254. @mock.patch('git_cl.Changelist.PostUploadUpdates')
  1255. @mock.patch('git_cl.Changelist._RunGitPushWithTraces')
  1256. @mock.patch('git_cl._UploadAllPrecheck')
  1257. @mock.patch('git_cl.Changelist.PrepareCherryPickSquashedCommit')
  1258. def test_upload_all_squashed_cherry_pick(self, mockCherryPickCommit,
  1259. mockUploadAllPrecheck,
  1260. mockRunGitPush,
  1261. mockPostUploadUpdates, *_mocks):
  1262. # Set up
  1263. cls = [
  1264. git_cl.Changelist(branchref='refs/heads/current-branch',
  1265. issue='12345'),
  1266. git_cl.Changelist(branchref='refs/heads/upstream-branch')
  1267. ]
  1268. mockUploadAllPrecheck.return_value = (cls, True)
  1269. upstream_gerrit_commit = 'upstream-commit'
  1270. scm.GIT.SetConfig('', 'branch.upstream-branch.gerritsquashhash',
  1271. upstream_gerrit_commit)
  1272. reviewers = []
  1273. ccs = []
  1274. commit_to_push = 'commit-to-push'
  1275. new_last_upload = 'new-last-upload'
  1276. change_desc = git_cl.ChangeDescription(
  1277. 'stonks/nChange-Id:ec15e81197380')
  1278. prev_patchset = 2
  1279. new_upload = git_cl._NewUpload(reviewers, ccs, commit_to_push,
  1280. new_last_upload, upstream_gerrit_commit,
  1281. change_desc, prev_patchset)
  1282. mockCherryPickCommit.return_value = new_upload
  1283. options = optparse.Values()
  1284. options.send_mail = options.private = False
  1285. options.squash = True
  1286. options.title = None
  1287. options.message = 'honk stonk'
  1288. options.topic = 'circus'
  1289. options.enable_auto_submit = False
  1290. options.set_bot_commit = False
  1291. options.cq_dry_run = False
  1292. options.use_commit_queue = False
  1293. options.hashtags = ['cow']
  1294. options.target_branch = None
  1295. options.push_options = ['uploadvalidator~skip']
  1296. orig_args = []
  1297. mockRunGitPush.return_value = (
  1298. 'remote: https://chromium-review.'
  1299. 'googlesource.com/c/chromium/circus/clown/+/1234 stonks')
  1300. # Call
  1301. git_cl.UploadAllSquashed(options, orig_args)
  1302. # Asserts
  1303. mockCherryPickCommit.assert_called_once_with(options,
  1304. upstream_gerrit_commit)
  1305. expected_refspec = ('commit-to-push:refs/for/refs/heads/main%'
  1306. 'm=honk_stonk,topic=circus,hashtag=cow')
  1307. expected_refspec_opts = ['m=honk_stonk', 'topic=circus', 'hashtag=cow']
  1308. mockRunGitPush.assert_called_once_with(expected_refspec,
  1309. expected_refspec_opts, mock.ANY,
  1310. options.push_options)
  1311. mockPostUploadUpdates.assert_called_once_with(options, new_upload,
  1312. '1234')
  1313. @mock.patch('git_cl.Changelist.GetGerritHost',
  1314. return_value='chromium-review.googlesource.com')
  1315. @mock.patch('git_cl.Changelist.GetRemoteBranch',
  1316. return_value=('origin', 'refs/remotes/origin/main'))
  1317. @mock.patch(
  1318. 'git_cl.Changelist.GetCommonAncestorWithUpstream',
  1319. side_effect=['current-upstream-ancestor', 'next-upstream-ancestor'])
  1320. @mock.patch('git_cl.Changelist.PostUploadUpdates')
  1321. @mock.patch('git_cl.Changelist._RunGitPushWithTraces')
  1322. @mock.patch('git_cl._UploadAllPrecheck')
  1323. @mock.patch('git_cl.Changelist.PrepareSquashedCommit')
  1324. def test_upload_all_squashed(self, mockSquashedCommit,
  1325. mockUploadAllPrecheck, mockRunGitPush,
  1326. mockPostUploadUpdates, *_mocks):
  1327. # Set up
  1328. cls = [
  1329. git_cl.Changelist(branchref='refs/heads/current-branch',
  1330. issue='12345'),
  1331. git_cl.Changelist(branchref='refs/heads/upstream-branch')
  1332. ]
  1333. mockUploadAllPrecheck.return_value = (cls, False)
  1334. reviewers = []
  1335. ccs = []
  1336. current_commit_to_push = 'commit-to-push'
  1337. current_new_last_upload = 'new-last-upload'
  1338. change_desc = git_cl.ChangeDescription(
  1339. 'stonks/nChange-Id:ec15e81197380')
  1340. prev_patchset = 2
  1341. new_upload_current = git_cl._NewUpload(reviewers, ccs,
  1342. current_commit_to_push,
  1343. current_new_last_upload,
  1344. 'next-upstream-ancestor',
  1345. change_desc, prev_patchset)
  1346. upstream_desc = git_cl.ChangeDescription('kwak')
  1347. upstream_parent = 'origin-commit'
  1348. upstream_new_last_upload = 'upstrea-last-upload'
  1349. upstream_commit_to_push = 'upstream_push_commit'
  1350. new_upload_upstream = git_cl._NewUpload(reviewers, ccs,
  1351. upstream_commit_to_push,
  1352. upstream_new_last_upload,
  1353. upstream_parent, upstream_desc,
  1354. prev_patchset)
  1355. mockSquashedCommit.side_effect = [
  1356. new_upload_upstream, new_upload_current
  1357. ]
  1358. options = optparse.Values()
  1359. options.send_mail = options.private = False
  1360. options.squash = True
  1361. options.title = None
  1362. options.message = 'honk stonk'
  1363. options.topic = 'circus'
  1364. options.enable_auto_submit = False
  1365. options.set_bot_commit = False
  1366. options.cq_dry_run = False
  1367. options.use_commit_queue = False
  1368. options.hashtags = ['cow']
  1369. options.target_branch = None
  1370. options.push_options = ['uploadvalidator~skip']
  1371. orig_args = []
  1372. mockRunGitPush.return_value = (
  1373. 'remote: https://chromium-review.'
  1374. 'googlesource.com/c/chromium/circus/clown/+/1233 kwak'
  1375. '\n'
  1376. 'remote: https://chromium-review.'
  1377. 'googlesource.com/c/chromium/circus/clown/+/1234 stonks')
  1378. # Call
  1379. git_cl.UploadAllSquashed(options, orig_args)
  1380. # Asserts
  1381. self.maxDiff = None
  1382. self.assertEqual(mockSquashedCommit.mock_calls, [
  1383. mock.call(options,
  1384. 'current-upstream-ancestor',
  1385. 'current-upstream-ancestor',
  1386. end_commit='next-upstream-ancestor'),
  1387. mock.call(options,
  1388. upstream_commit_to_push,
  1389. 'next-upstream-ancestor',
  1390. end_commit=None)
  1391. ])
  1392. expected_refspec = ('commit-to-push:refs/for/refs/heads/main%'
  1393. 'topic=circus,hashtag=cow')
  1394. expected_refspec_opts = ['topic=circus', 'hashtag=cow']
  1395. mockRunGitPush.assert_called_once_with(expected_refspec,
  1396. expected_refspec_opts, mock.ANY,
  1397. options.push_options)
  1398. self.assertEqual(mockPostUploadUpdates.mock_calls, [
  1399. mock.call(options, new_upload_upstream, '1233'),
  1400. mock.call(options, new_upload_current, '1234')
  1401. ])
  1402. @mock.patch('git_cl.Changelist.GetGerritHost',
  1403. return_value='chromium-review.googlesource.com')
  1404. @mock.patch('git_cl.Changelist.GetRemoteBranch',
  1405. return_value=('origin', 'refs/remotes/origin/main'))
  1406. @mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream',
  1407. return_value='current-upstream-ancestor')
  1408. @mock.patch('git_cl.Changelist._UpdateWithExternalChanges')
  1409. @mock.patch('git_cl.Changelist.PostUploadUpdates')
  1410. @mock.patch('git_cl.Changelist._RunGitPushWithTraces')
  1411. @mock.patch('git_cl._UploadAllPrecheck')
  1412. @mock.patch('git_cl.Changelist.PrepareSquashedCommit')
  1413. def test_upload_all_squashed_external_changes(self, mockSquashedCommit,
  1414. mockUploadAllPrecheck,
  1415. mockRunGitPush,
  1416. mockPostUploadUpdates,
  1417. mockExternalChanges, *_mocks):
  1418. options = optparse.Values()
  1419. options.send_mail = options.private = False
  1420. options.squash = True
  1421. options.title = None
  1422. options.topic = 'circus'
  1423. options.message = 'honk stonk'
  1424. options.enable_auto_submit = False
  1425. options.set_bot_commit = False
  1426. options.cq_dry_run = False
  1427. options.use_commit_queue = False
  1428. options.hashtags = ['cow']
  1429. options.target_branch = None
  1430. options.push_options = ['uploadvalidator~skip']
  1431. orig_args = []
  1432. cls = [
  1433. git_cl.Changelist(branchref='refs/heads/current-branch',
  1434. issue='12345')
  1435. ]
  1436. mockUploadAllPrecheck.return_value = (cls, False)
  1437. reviewers = []
  1438. ccs = []
  1439. # Test case: user wants to pull in external changes.
  1440. mockExternalChanges.reset_mock()
  1441. mockExternalChanges.return_value = None
  1442. current_commit_to_push = 'commit-to-push'
  1443. current_new_last_upload = 'new-last-upload'
  1444. change_desc = git_cl.ChangeDescription(
  1445. 'stonks/nChange-Id:ec15e81197380')
  1446. prev_patchset = 2
  1447. new_upload_current = git_cl._NewUpload(reviewers, ccs,
  1448. current_commit_to_push,
  1449. current_new_last_upload,
  1450. 'next-upstream-ancestor',
  1451. change_desc, prev_patchset)
  1452. mockSquashedCommit.return_value = new_upload_current
  1453. mockRunGitPush.return_value = (
  1454. 'remote: https://chromium-review.'
  1455. 'googlesource.com/c/chromium/circus/clown/+/1233 kwak')
  1456. # Test case: user wants to pull in external changes.
  1457. mockExternalChanges.reset_mock()
  1458. mockExternalChanges.return_value = 'external-commit'
  1459. # Call
  1460. git_cl.UploadAllSquashed(options, orig_args)
  1461. # Asserts
  1462. self.assertEqual(mockSquashedCommit.mock_calls, [
  1463. mock.call(
  1464. options, 'external-commit', 'external-commit', end_commit=None)
  1465. ])
  1466. expected_refspec = ('commit-to-push:refs/for/refs/heads/main%'
  1467. 'm=honk_stonk,topic=circus,hashtag=cow')
  1468. expected_refspec_opts = ['m=honk_stonk', 'topic=circus', 'hashtag=cow']
  1469. mockRunGitPush.assert_called_once_with(expected_refspec,
  1470. expected_refspec_opts, mock.ANY,
  1471. options.push_options)
  1472. self.assertEqual(mockPostUploadUpdates.mock_calls,
  1473. [mock.call(options, new_upload_current, '1233')])
  1474. # Test case: user does not want external changes or there are none.
  1475. mockSquashedCommit.reset_mock()
  1476. mockExternalChanges.return_value = None
  1477. # Call
  1478. git_cl.UploadAllSquashed(options, orig_args)
  1479. # Asserts
  1480. self.assertEqual(mockSquashedCommit.mock_calls, [
  1481. mock.call(options,
  1482. 'current-upstream-ancestor',
  1483. 'current-upstream-ancestor',
  1484. end_commit=None)
  1485. ])
  1486. @mock.patch('git_cl.Changelist._GerritCommitMsgHookCheck',
  1487. lambda offer_removal: None)
  1488. @mock.patch('git_cl.RunGit')
  1489. @mock.patch('git_cl.RunGitSilent')
  1490. @mock.patch('git_cl.Changelist._GitGetBranchConfigValue')
  1491. @mock.patch('git_cl.Changelist.FetchUpstreamTuple')
  1492. @mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
  1493. @mock.patch('scm.GIT.GetBranchRef')
  1494. @mock.patch('git_cl.Changelist.GetRemoteBranch')
  1495. @mock.patch('scm.GIT.IsAncestor')
  1496. @mock.patch('gclient_utils.AskForData')
  1497. def test_upload_all_precheck_long_chain(
  1498. self, mockAskForData, mockIsAncestor, mockGetRemoteBranch,
  1499. mockGetBranchRef, mockGetCommonAncestorWithUpstream,
  1500. mockFetchUpstreamTuple, mockGitGetBranchConfigValue,
  1501. mockRunGitSilent, mockRunGit, *_mocks):
  1502. mockGetRemoteBranch.return_value = ('origin',
  1503. 'refs/remotes/origin/main')
  1504. branches = [
  1505. 'current', 'upstream3', 'blank3', 'blank2', 'upstream2', 'blank1',
  1506. 'upstream1', 'origin/main'
  1507. ]
  1508. mockGetBranchRef.side_effect = (
  1509. ['refs/heads/current'] + # detached HEAD check
  1510. ['refs/heads/%s' % b for b in branches])
  1511. mockGetCommonAncestorWithUpstream.side_effect = [
  1512. 'commit3.5',
  1513. 'commit3.5',
  1514. 'commit3.5',
  1515. 'commit2.5',
  1516. 'commit1.5',
  1517. 'commit1.5',
  1518. 'commit0.5',
  1519. ]
  1520. mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
  1521. ('.', 'refs/heads/blank3'),
  1522. ('.', 'refs/heads/blank2'),
  1523. ('.', 'refs/heads/upstream2'),
  1524. ('.', 'refs/heads/blank1'),
  1525. ('.', 'refs/heads/upstream1'),
  1526. ('origin',
  1527. 'refs/heads/origin/main')]
  1528. # end commits
  1529. mockRunGit.side_effect = [
  1530. 'commit4', 'commit3.5', 'commit3.5', 'commit2', 'commit1.5',
  1531. 'commit1', 'commit0.5'
  1532. ]
  1533. mockRunGitSilent.side_effect = ['80', '81', '0', '0', '82', '0', '83']
  1534. # Get gerrit squash hash. We only check this for branches that have a
  1535. # diff. Set to None to trigger `must_upload_upstream`.
  1536. mockGitGetBranchConfigValue.return_value = None
  1537. options = optparse.Values()
  1538. options.force = False
  1539. options.cherry_pick_stacked = False
  1540. orig_args = ['--preserve-tryjobs', '--chicken']
  1541. # Case 2: upstream3 has never been uploaded.
  1542. # (so no LAST_UPLOAD_HASH_CONIFG_KEY)
  1543. # Case 4: upstream2's last_upload is behind upstream3's base_commit
  1544. key = f'branch.upstream2.{git_cl.LAST_UPLOAD_HASH_CONFIG_KEY}'
  1545. scm.GIT.SetConfig('', key, 'commit2.3')
  1546. mockIsAncestor.side_effect = [True]
  1547. # Case 3: upstream1's last_upload matches upstream2's base_commit
  1548. key = f'branch.upstream1.{git_cl.LAST_UPLOAD_HASH_CONFIG_KEY}'
  1549. scm.GIT.SetConfig('', key, 'commit1.5')
  1550. cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
  1551. self.assertFalse(cherry_pick)
  1552. mockAskForData.assert_called_once_with(
  1553. "\noptions ['--preserve-tryjobs', '--chicken'] will be used for all "
  1554. "uploads.\nAt least one parent branch in `current, upstream3, "
  1555. "upstream2` has never been uploaded and must be uploaded before/with "
  1556. "`upstream3`.\nPress Enter to confirm, or Ctrl+C to abort")
  1557. self.assertEqual(len(cls), 3)
  1558. @mock.patch('git_cl.Changelist._GerritCommitMsgHookCheck',
  1559. lambda offer_removal: None)
  1560. @mock.patch('git_cl.RunGit')
  1561. @mock.patch('git_cl.RunGitSilent')
  1562. @mock.patch('git_cl.Changelist._GitGetBranchConfigValue')
  1563. @mock.patch('git_cl.Changelist.FetchUpstreamTuple')
  1564. @mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
  1565. @mock.patch('scm.GIT.GetBranchRef')
  1566. @mock.patch('git_cl.Changelist.GetRemoteBranch')
  1567. @mock.patch('scm.GIT.IsAncestor')
  1568. @mock.patch('gclient_utils.AskForData')
  1569. def test_upload_all_precheck_options_must_upload(
  1570. self, mockAskForData, mockIsAncestor, mockGetRemoteBranch,
  1571. mockGetBranchRef, mockGetCommonAncestorWithUpstream,
  1572. mockFetchUpstreamTuple, mockGitGetBranchConfigValue,
  1573. mockRunGitSilent, mockRunGit, *_mocks):
  1574. mockGetRemoteBranch.return_value = ('origin',
  1575. 'refs/remotes/origin/main')
  1576. branches = ['current', 'upstream3', 'main']
  1577. mockGetBranchRef.side_effect = (
  1578. ['refs/heads/current'] + # detached HEAD check
  1579. ['refs/heads/%s' % b for b in branches])
  1580. mockGetCommonAncestorWithUpstream.side_effect = [
  1581. 'commit3.5', 'commit0.5'
  1582. ]
  1583. mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
  1584. ('origin', 'refs/heads/main')]
  1585. mockIsAncestor.return_value = True
  1586. # end commits
  1587. mockRunGit.return_value = 'any-commit'
  1588. mockRunGitSilent.return_value = '42'
  1589. # Get gerrit squash hash. We only check this for branches that have a
  1590. # diff.
  1591. mockGitGetBranchConfigValue.return_value = None
  1592. # Test case: User wants to cherry pick, but all branches must be
  1593. # uploaded.
  1594. options = optparse.Values()
  1595. options.force = True
  1596. options.cherry_pick_stacked = True
  1597. orig_args = []
  1598. with self.assertRaises(SystemExitMock):
  1599. git_cl._UploadAllPrecheck(options, orig_args)
  1600. # Test case: User does not require cherry picking
  1601. options.cherry_pick_stacked = False
  1602. # reset side_effects
  1603. mockGetBranchRef.side_effect = (
  1604. ['refs/heads/current'] + # detached HEAD check
  1605. ['refs/heads/%s' % b for b in branches])
  1606. mockGetCommonAncestorWithUpstream.side_effect = [
  1607. 'commit3.5', 'commit0.5'
  1608. ]
  1609. mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
  1610. ('origin', 'refs/heads/main')]
  1611. cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
  1612. self.assertFalse(cherry_pick)
  1613. self.assertEqual(len(cls), 2)
  1614. mockAskForData.assert_not_called()
  1615. # Test case: User does not require cherry picking and not in force mode.
  1616. options.force = False
  1617. # reset side_effects
  1618. mockGetBranchRef.side_effect = (
  1619. ['refs/heads/current'] + # detached HEAD check
  1620. ['refs/heads/%s' % b for b in branches])
  1621. mockGetCommonAncestorWithUpstream.side_effect = [
  1622. 'commit3.5', 'commit0.5'
  1623. ]
  1624. mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
  1625. ('origin', 'refs/heads/main')]
  1626. cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
  1627. self.assertFalse(cherry_pick)
  1628. self.assertEqual(len(cls), 2)
  1629. mockAskForData.assert_called_once()
  1630. @mock.patch('git_cl.Changelist._GerritCommitMsgHookCheck',
  1631. lambda offer_removal: None)
  1632. @mock.patch('git_cl.RunGit')
  1633. @mock.patch('git_cl.RunGitSilent')
  1634. @mock.patch('git_cl.Changelist._GitGetBranchConfigValue')
  1635. @mock.patch('git_cl.Changelist.FetchUpstreamTuple')
  1636. @mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
  1637. @mock.patch('scm.GIT.GetBranchRef')
  1638. @mock.patch('scm.GIT.IsAncestor')
  1639. @mock.patch('gclient_utils.AskForData')
  1640. def test_upload_all_precheck_must_rebase(
  1641. self, mockAskForData, mockIsAncestor, mockGetBranchRef,
  1642. mockGetCommonAncestorWithUpstream, mockFetchUpstreamTuple,
  1643. mockGitGetBranchConfigValue, mockRunGitSilent, mockRunGit, *_mocks):
  1644. branches = ['current', 'upstream3']
  1645. mockGetBranchRef.side_effect = ['refs/heads/%s' % b for b in branches]
  1646. mockGetCommonAncestorWithUpstream.return_value = 'commit3.5'
  1647. mockFetchUpstreamTuple.return_value = ('.', 'refs/heads/upstream3')
  1648. # end commits
  1649. mockRunGit.return_value = 'commit4'
  1650. mockRunGitSilent.return_value = '42'
  1651. # Get gerrit squash hash. We only check this for branches that have a
  1652. # diff. Set to None to trigger `must_upload_upstream`.
  1653. mockGitGetBranchConfigValue.return_value = None
  1654. # Case 5: current's base_commit is behind upstream3's last_upload.
  1655. key = f'branch.upstream3.{git_cl.LAST_UPLOAD_HASH_CONFIG_KEY}'
  1656. scm.GIT.SetConfig('', key, 'commit3.7')
  1657. mockIsAncestor.side_effect = [False, True]
  1658. with self.assertRaises(SystemExitMock):
  1659. options = optparse.Values()
  1660. options.force = False
  1661. options.cherry_pick_stacked = False
  1662. git_cl._UploadAllPrecheck(options, [])
  1663. @mock.patch('git_cl.Changelist._GerritCommitMsgHookCheck',
  1664. lambda offer_removal: None)
  1665. @mock.patch('git_cl.RunGit')
  1666. @mock.patch('git_cl.RunGitSilent')
  1667. @mock.patch('git_cl.Changelist._GitGetBranchConfigValue')
  1668. @mock.patch('git_cl.Changelist.FetchUpstreamTuple')
  1669. @mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
  1670. @mock.patch('scm.GIT.GetBranchRef')
  1671. @mock.patch('git_cl.Changelist.GetRemoteBranch')
  1672. @mock.patch('scm.GIT.IsAncestor')
  1673. @mock.patch('gclient_utils.AskForData')
  1674. def test_upload_all_precheck_hit_main(
  1675. self, mockAskForData, mockIsAncestor, mockGetRemoteBranch,
  1676. mockGetBranchRef, mockGetCommonAncestorWithUpstream,
  1677. mockFetchUpstreamTuple, mockGitGetBranchConfigValue,
  1678. mockRunGitSilent, mockRunGit, *_mocks):
  1679. options = optparse.Values()
  1680. options.force = False
  1681. options.cherry_pick_stacked = False
  1682. orig_args = ['--preserve-tryjobs', '--chicken']
  1683. mockGetRemoteBranch.return_value = ('origin',
  1684. 'refs/remotes/origin/main')
  1685. branches = ['current', 'upstream3', 'main']
  1686. mockGetBranchRef.side_effect = (
  1687. ['refs/heads/current'] + # detached HEAD check
  1688. ['refs/heads/%s' % b for b in branches])
  1689. mockGetCommonAncestorWithUpstream.side_effect = [
  1690. 'commit3.5', 'commit0.5'
  1691. ]
  1692. mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
  1693. ('origin', 'refs/heads/main')]
  1694. mockIsAncestor.return_value = True
  1695. # Give upstream3 a last upload hash
  1696. key = f'branch.upstream3.{git_cl.LAST_UPLOAD_HASH_CONFIG_KEY}'
  1697. scm.GIT.SetConfig('', key, 'commit3.4')
  1698. # end commits
  1699. mockRunGit.return_value = 'commit4'
  1700. mockRunGitSilent.return_value = '42'
  1701. # Get gerrit squash hash. We only check this for branches that have a
  1702. # diff.
  1703. mockGitGetBranchConfigValue.return_value = 'just needs to exist'
  1704. # Test case: user cherry picks with options
  1705. options.cherry_pick_stacked = True
  1706. # Reset side_effects
  1707. mockGetBranchRef.side_effect = (
  1708. ['refs/heads/current'] + # detached HEAD check
  1709. ['refs/heads/%s' % b for b in branches])
  1710. mockGetCommonAncestorWithUpstream.side_effect = [
  1711. 'commit3.5', 'commit0.5'
  1712. ]
  1713. mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
  1714. ('origin', 'refs/heads/main')]
  1715. cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
  1716. self.assertTrue(cherry_pick)
  1717. self.assertEqual(len(cls), 2)
  1718. mockAskForData.assert_not_called()
  1719. # Test case: user uses force, no cherry-pick.
  1720. options.cherry_pick_stacked = False
  1721. options.force = True
  1722. # Reset side_effects
  1723. mockGetBranchRef.side_effect = (
  1724. ['refs/heads/current'] + # detached HEAD check
  1725. ['refs/heads/%s' % b for b in branches])
  1726. mockGetCommonAncestorWithUpstream.side_effect = [
  1727. 'commit3.5', 'commit0.5'
  1728. ]
  1729. mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
  1730. ('origin', 'refs/heads/main')]
  1731. cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
  1732. self.assertFalse(cherry_pick)
  1733. self.assertEqual(len(cls), 2)
  1734. mockAskForData.assert_not_called()
  1735. # Test case: user wants to cherry pick after being asked.
  1736. mockAskForData.return_value = 'n'
  1737. options.cherry_pick_stacked = False
  1738. options.force = False
  1739. # Reset side_effects
  1740. mockGetBranchRef.side_effect = (
  1741. ['refs/heads/current'] + # detached HEAD check
  1742. ['refs/heads/%s' % b for b in branches])
  1743. mockGetCommonAncestorWithUpstream.side_effect = [
  1744. 'commit3.5', 'commit0.5'
  1745. ]
  1746. mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
  1747. ('origin', 'refs/heads/main')]
  1748. cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
  1749. self.assertTrue(cherry_pick)
  1750. self.assertEqual(len(cls), 2)
  1751. mockAskForData.assert_called_once_with(
  1752. "\noptions ['--preserve-tryjobs', '--chicken'] will be used for all "
  1753. "uploads.\n"
  1754. "Press enter to update branches current, upstream3.\n"
  1755. "Or type `n` to upload only `current` cherry-picked on upstream3's "
  1756. "last upload:")
  1757. @mock.patch('git_cl.Changelist._GerritCommitMsgHookCheck',
  1758. lambda offer_removal: None)
  1759. @mock.patch('git_cl.RunGit')
  1760. @mock.patch('git_cl.RunGitSilent')
  1761. @mock.patch('git_cl.Changelist._GitGetBranchConfigValue')
  1762. @mock.patch('git_cl.Changelist.FetchUpstreamTuple')
  1763. @mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
  1764. @mock.patch('scm.GIT.GetBranchRef')
  1765. @mock.patch('git_cl.Changelist.GetRemoteBranch')
  1766. @mock.patch('scm.GIT.IsAncestor')
  1767. @mock.patch('gclient_utils.AskForData')
  1768. def test_upload_all_precheck_one_change(
  1769. self, mockAskForData, mockIsAncestor, mockGetRemoteBranch,
  1770. mockGetBranchRef, mockGetCommonAncestorWithUpstream,
  1771. mockFetchUpstreamTuple, mockGitGetBranchConfigValue,
  1772. mockRunGitSilent, mockRunGit, *_mocks):
  1773. options = optparse.Values()
  1774. options.force = False
  1775. options.cherry_pick_stacked = False
  1776. orig_args = ['--preserve-tryjobs', '--chicken']
  1777. mockGetRemoteBranch.return_value = ('origin',
  1778. 'refs/remotes/origin/main')
  1779. mockGetBranchRef.side_effect = [
  1780. 'refs/heads/current', # detached HEAD check
  1781. 'refs/heads/current', # call within while loop
  1782. 'refs/heads/main',
  1783. 'refs/heads/main'
  1784. ]
  1785. mockGetCommonAncestorWithUpstream.return_value = 'commit3.5'
  1786. mockFetchUpstreamTuple.return_value = ('', 'refs/heads/main')
  1787. mockIsAncestor.return_value = True
  1788. # end commits
  1789. mockRunGit.return_value = 'commit4'
  1790. mockRunGitSilent.return_value = '42'
  1791. # Get gerrit squash hash. We only check this for branches that have a
  1792. # diff. Set to None to trigger `must_upload_upstream`.
  1793. mockGitGetBranchConfigValue.return_value = 'does not matter'
  1794. # Case 1: We hit the main branch
  1795. cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
  1796. self.assertFalse(cherry_pick)
  1797. self.assertEqual(len(cls), 1)
  1798. mockAskForData.assert_not_called()
  1799. # No diff for current change
  1800. mockRunGitSilent.return_value = '0'
  1801. with self.assertRaises(SystemExitMock):
  1802. git_cl._UploadAllPrecheck(options, orig_args)
  1803. @mock.patch('scm.GIT.GetBranchRef', return_value=None)
  1804. def test_upload_all_precheck_detached_HEAD(self, mockGetBranchRef):
  1805. with self.assertRaises(SystemExitMock):
  1806. git_cl._UploadAllPrecheck(optparse.Values(), [])
  1807. @mock.patch('git_cl.RunGit')
  1808. @mock.patch('git_cl.CMDupload')
  1809. @mock.patch('sys.stdin', io.StringIO('\n'))
  1810. @mock.patch('sys.stdout', io.StringIO())
  1811. def test_upload_branch_deps(self, *_mocks):
  1812. def mock_run_git(*args, **_kwargs):
  1813. if args[0] == [
  1814. 'for-each-ref',
  1815. '--format=%(refname:short) %(upstream:short)', 'refs/heads'
  1816. ]:
  1817. # Create a local branch dependency tree that looks like this:
  1818. # test1 -> test2 -> test3 -> test4 -> test5
  1819. # -> test3.1
  1820. # test6 -> test0
  1821. branch_deps = [
  1822. 'test2 test1', # test1 -> test2
  1823. 'test3 test2', # test2 -> test3
  1824. 'test3.1 test2', # test2 -> test3.1
  1825. 'test4 test3', # test3 -> test4
  1826. 'test5 test4', # test4 -> test5
  1827. 'test6 test0', # test0 -> test6
  1828. 'test7', # test7
  1829. ]
  1830. return '\n'.join(branch_deps)
  1831. git_cl.RunGit.side_effect = mock_run_git
  1832. git_cl.CMDupload.return_value = 0
  1833. class MockChangelist():
  1834. def __init__(self):
  1835. pass
  1836. def GetBranch(self):
  1837. return 'test1'
  1838. def GetIssue(self):
  1839. return '123'
  1840. def GetPatchset(self):
  1841. return '1001'
  1842. def IsGerrit(self):
  1843. return False
  1844. ret = git_cl.upload_branch_deps(MockChangelist(), [])
  1845. # CMDupload should have been called 5 times because of 5 dependent
  1846. # branches.
  1847. self.assertEqual(5, len(git_cl.CMDupload.mock_calls))
  1848. self.assertEqual(0, ret)
  1849. def test_gerrit_change_id(self):
  1850. self.calls = [
  1851. ((['git', 'write-tree'], ), 'hashtree'),
  1852. ((['git', 'rev-parse', 'HEAD~0'], ), 'branch-parent'),
  1853. ((['git', 'var',
  1854. 'GIT_AUTHOR_IDENT'], ), 'A B <a@b.org> 1456848326 +0100'),
  1855. ((['git', 'var',
  1856. 'GIT_COMMITTER_IDENT'], ), 'C D <c@d.org> 1456858326 +0100'),
  1857. ((['git', 'hash-object', '-t', 'commit',
  1858. '--stdin'], ), 'hashchange'),
  1859. ]
  1860. change_id = git_cl.GenerateGerritChangeId('line1\nline2\n')
  1861. self.assertEqual(change_id, 'Ihashchange')
  1862. @mock.patch('gerrit_util.IsCodeOwnersEnabledOnHost')
  1863. @mock.patch('git_cl.Settings.GetBugPrefix')
  1864. @mock.patch('git_cl.Changelist.FetchDescription')
  1865. @mock.patch('git_cl.Changelist.GetBranch')
  1866. @mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
  1867. @mock.patch('git_cl.Changelist.GetGerritHost')
  1868. @mock.patch('git_cl.Changelist.GetGerritProject')
  1869. @mock.patch('git_cl.Changelist.GetRemoteBranch')
  1870. @mock.patch('owners_client.OwnersClient.BatchListOwners')
  1871. def getDescriptionForUploadTest(self,
  1872. mockBatchListOwners=None,
  1873. mockGetRemoteBranch=None,
  1874. mockGetGerritProject=None,
  1875. mockGetGerritHost=None,
  1876. mockGetCommonAncestorWithUpstream=None,
  1877. mockGetBranch=None,
  1878. mockFetchDescription=None,
  1879. mockGetBugPrefix=None,
  1880. mockIsCodeOwnersEnabledOnHost=None,
  1881. initial_description='desc',
  1882. commit_description=None,
  1883. bug=None,
  1884. fixed=None,
  1885. branch='branch',
  1886. reviewers=None,
  1887. add_owners_to=None,
  1888. expected_description='desc'):
  1889. reviewers = reviewers or []
  1890. owners_by_path = {
  1891. 'a': ['a@example.com'],
  1892. 'b': ['b@example.com'],
  1893. 'c': ['c@example.com'],
  1894. }
  1895. mockIsCodeOwnersEnabledOnHost.return_value = True
  1896. mockGetBranch.return_value = branch
  1897. mockGetBugPrefix.return_value = 'prefix'
  1898. mockGetCommonAncestorWithUpstream.return_value = 'upstream'
  1899. mockGetRemoteBranch.return_value = ('origin',
  1900. 'refs/remotes/origin/main')
  1901. mockFetchDescription.return_value = 'desc'
  1902. mockBatchListOwners.side_effect = lambda ps: {
  1903. p: owners_by_path.get(p)
  1904. for p in ps
  1905. }
  1906. cl = git_cl.Changelist(issue=1234)
  1907. actual = cl._GetDescriptionForUpload(options=mock.Mock(
  1908. bug=bug,
  1909. fixed=fixed,
  1910. reviewers=reviewers,
  1911. add_owners_to=add_owners_to,
  1912. message=initial_description,
  1913. commit_description=commit_description),
  1914. git_diff_args=None,
  1915. files=list(owners_by_path))
  1916. self.assertEqual(expected_description, actual.description)
  1917. def testGetDescriptionForUpload(self):
  1918. self.getDescriptionForUploadTest()
  1919. def testGetDescriptionForUpload_Bug(self):
  1920. self.getDescriptionForUploadTest(bug='1234',
  1921. expected_description='\n'.join([
  1922. 'desc',
  1923. '',
  1924. 'Bug: prefix:1234',
  1925. ]))
  1926. def testGetDescriptionForUpload_Fixed(self):
  1927. self.getDescriptionForUploadTest(fixed='1234',
  1928. expected_description='\n'.join([
  1929. 'desc',
  1930. '',
  1931. 'Fixed: prefix:1234',
  1932. ]))
  1933. @mock.patch('git_cl.Changelist.GetIssue')
  1934. def testGetDescriptionForUpload_BugFromBranch(self, mockGetIssue):
  1935. mockGetIssue.return_value = None
  1936. self.getDescriptionForUploadTest(branch='bug-1234',
  1937. expected_description='\n'.join([
  1938. 'desc',
  1939. '',
  1940. 'Bug: prefix:1234',
  1941. ]))
  1942. @mock.patch('git_cl.Changelist.GetIssue')
  1943. def testGetDescriptionForUpload_FixedFromBranch(self, mockGetIssue):
  1944. mockGetIssue.return_value = None
  1945. self.getDescriptionForUploadTest(branch='fix-1234',
  1946. expected_description='\n'.join([
  1947. 'desc',
  1948. '',
  1949. 'Fixed: prefix:1234',
  1950. ]))
  1951. def testGetDescriptionForUpload_SkipBugFromBranchIfAlreadyUploaded(self):
  1952. self.getDescriptionForUploadTest(
  1953. branch='bug-1234',
  1954. expected_description='desc',
  1955. )
  1956. def testGetDescriptionForUpload_AddOwnersToR(self):
  1957. self.getDescriptionForUploadTest(
  1958. reviewers=['a@example.com'],
  1959. add_owners_to='R',
  1960. expected_description='\n'.join([
  1961. 'desc',
  1962. '',
  1963. 'R=a@example.com, b@example.com, c@example.com',
  1964. ]))
  1965. def testGetDescriptionForUpload_AddOwnersToNoOwnersNeeded(self):
  1966. self.getDescriptionForUploadTest(
  1967. reviewers=['a@example.com', 'c@example.com'],
  1968. expected_description='\n'.join([
  1969. 'desc',
  1970. '',
  1971. 'R=a@example.com, c@example.com',
  1972. ]))
  1973. def testGetDescriptionForUpload_Reviewers(self):
  1974. self.getDescriptionForUploadTest(
  1975. reviewers=['a@example.com', 'b@example.com'],
  1976. expected_description='\n'.join([
  1977. 'desc',
  1978. '',
  1979. 'R=a@example.com, b@example.com',
  1980. ]))
  1981. def testGetDescriptionForUpload_NewDesc(self):
  1982. self.getDescriptionForUploadTest(
  1983. commit_description='this is a new desc',
  1984. expected_description='this is a new desc')
  1985. @mock.patch('sys.stdin', io.StringIO('this is a new desc'))
  1986. def testGetDescriptionForUpload_NewDescFromStdin(self):
  1987. self.getDescriptionForUploadTest(
  1988. commit_description='-', expected_description='this is a new desc')
  1989. def test_description_append_footer(self):
  1990. for init_desc, footer_line, expected_desc in [
  1991. # Use unique desc first lines for easy test failure
  1992. # identification.
  1993. ('foo', 'R=one', 'foo\n\nR=one'),
  1994. ('foo\n\nR=one', 'BUG=', 'foo\n\nR=one\nBUG='),
  1995. ('foo\n\nR=one', 'Change-Id: Ixx',
  1996. 'foo\n\nR=one\n\nChange-Id: Ixx'),
  1997. ('foo\n\nChange-Id: Ixx', 'R=one',
  1998. 'foo\n\nR=one\n\nChange-Id: Ixx'),
  1999. ('foo\n\nR=one\n\nChange-Id: Ixx', 'Foo-Bar: baz',
  2000. 'foo\n\nR=one\n\nChange-Id: Ixx\nFoo-Bar: baz'),
  2001. ('foo\n\nChange-Id: Ixx', 'Foo-Bak: baz',
  2002. 'foo\n\nChange-Id: Ixx\nFoo-Bak: baz'),
  2003. ('foo', 'Change-Id: Ixx', 'foo\n\nChange-Id: Ixx'),
  2004. ]:
  2005. desc = git_cl.ChangeDescription(init_desc)
  2006. desc.append_footer(footer_line)
  2007. self.assertEqual(desc.description, expected_desc)
  2008. def test_update_reviewers(self):
  2009. data = [
  2010. ('foo', [], 'foo'),
  2011. ('foo\nR=xx', [], 'foo\nR=xx'),
  2012. ('foo', ['a@c'], 'foo\n\nR=a@c'),
  2013. ('foo\nR=xx', ['a@c'], 'foo\n\nR=a@c, xx'),
  2014. ('foo\nBUG=', ['a@c'], 'foo\nBUG=\nR=a@c'),
  2015. ('foo\nR=xx\nR=bar', ['a@c'], 'foo\n\nR=a@c, bar, xx'),
  2016. ('foo', ['a@c', 'b@c'], 'foo\n\nR=a@c, b@c'),
  2017. ('foo\nBar\n\nR=\nBUG=', ['c@c'], 'foo\nBar\n\nR=c@c\nBUG='),
  2018. ('foo\nBar\n\nR=\nBUG=\nR=', ['c@c'], 'foo\nBar\n\nR=c@c\nBUG='),
  2019. # Same as the line before, but full of whitespaces.
  2020. (
  2021. 'foo\nBar\n\n R = \n BUG = \n R = ',
  2022. ['c@c'],
  2023. 'foo\nBar\n\nR=c@c\n BUG =',
  2024. ),
  2025. # Whitespaces aren't interpreted as new lines.
  2026. ('foo BUG=allo R=joe ', ['c@c'], 'foo BUG=allo R=joe\n\nR=c@c'),
  2027. ]
  2028. expected = [i[-1] for i in data]
  2029. actual = []
  2030. for orig, reviewers, _expected in data:
  2031. obj = git_cl.ChangeDescription(orig)
  2032. obj.update_reviewers(reviewers)
  2033. actual.append(obj.description)
  2034. self.assertEqual(expected, actual)
  2035. def test_get_hash_tags(self):
  2036. cases = [
  2037. ('', []),
  2038. ('a', []),
  2039. ('[a]', ['a']),
  2040. ('[aa]', ['aa']),
  2041. ('[a ]', ['a']),
  2042. ('[a- ]', ['a']),
  2043. ('[a- b]', ['a-b']),
  2044. ('[a--b]', ['a-b']),
  2045. ('[a', []),
  2046. ('[a]x', ['a']),
  2047. ('[aa]x', ['aa']),
  2048. ('[a b]', ['a-b']),
  2049. ('[a b]', ['a-b']),
  2050. ('[a__b]', ['a-b']),
  2051. ('[a] x', ['a']),
  2052. ('[a][b]', ['a', 'b']),
  2053. ('[a] [b]', ['a', 'b']),
  2054. ('[a][b]x', ['a', 'b']),
  2055. ('[a][b] x', ['a', 'b']),
  2056. ('[a]\n[b]', ['a']),
  2057. ('[a\nb]', []),
  2058. ('[a][', ['a']),
  2059. ('Revert "[a] feature"', ['a']),
  2060. ('Reland "[a] feature"', ['a']),
  2061. ('Revert: [a] feature', ['a']),
  2062. ('Reland: [a] feature', ['a']),
  2063. ('Revert "Reland: [a] feature"', ['a']),
  2064. ('Foo: feature', ['foo']),
  2065. ('Foo Bar: feature', ['foo-bar']),
  2066. ('Change Foo::Bar', []),
  2067. ('Foo: Change Foo::Bar', ['foo']),
  2068. ('Revert "Foo bar: feature"', ['foo-bar']),
  2069. ('Reland "Foo bar: feature"', ['foo-bar']),
  2070. ]
  2071. for desc, expected in cases:
  2072. change_desc = git_cl.ChangeDescription(desc)
  2073. actual = change_desc.get_hash_tags()
  2074. self.assertEqual(
  2075. actual, expected,
  2076. 'GetHashTags(%r) == %r, expected %r' % (desc, actual, expected))
  2077. self.assertEqual(None, git_cl.GetTargetRef('origin', None, 'main'))
  2078. self.assertEqual(
  2079. None, git_cl.GetTargetRef(None, 'refs/remotes/origin/main', 'main'))
  2080. # Check default target refs for branches.
  2081. self.assertEqual(
  2082. 'refs/heads/main',
  2083. git_cl.GetTargetRef('origin', 'refs/remotes/origin/main', None))
  2084. self.assertEqual(
  2085. 'refs/heads/main',
  2086. git_cl.GetTargetRef('origin', 'refs/remotes/origin/lkgr', None))
  2087. self.assertEqual(
  2088. 'refs/heads/main',
  2089. git_cl.GetTargetRef('origin', 'refs/remotes/origin/lkcr', None))
  2090. self.assertEqual(
  2091. 'refs/branch-heads/123',
  2092. git_cl.GetTargetRef('origin', 'refs/remotes/branch-heads/123',
  2093. None))
  2094. self.assertEqual(
  2095. 'refs/diff/test',
  2096. git_cl.GetTargetRef('origin', 'refs/remotes/origin/refs/diff/test',
  2097. None))
  2098. self.assertEqual(
  2099. 'refs/heads/chrome/m42',
  2100. git_cl.GetTargetRef('origin', 'refs/remotes/origin/chrome/m42',
  2101. None))
  2102. # Check target refs for user-specified target branch.
  2103. for branch in ('branch-heads/123', 'remotes/branch-heads/123',
  2104. 'refs/remotes/branch-heads/123'):
  2105. self.assertEqual(
  2106. 'refs/branch-heads/123',
  2107. git_cl.GetTargetRef('origin', 'refs/remotes/origin/main',
  2108. branch))
  2109. for branch in ('origin/main', 'remotes/origin/main',
  2110. 'refs/remotes/origin/main'):
  2111. self.assertEqual(
  2112. 'refs/heads/main',
  2113. git_cl.GetTargetRef('origin', 'refs/remotes/branch-heads/123',
  2114. branch))
  2115. for branch in ('main', 'heads/main', 'refs/heads/main'):
  2116. self.assertEqual(
  2117. 'refs/heads/main',
  2118. git_cl.GetTargetRef('origin', 'refs/remotes/branch-heads/123',
  2119. branch))
  2120. @mock.patch('git_common.is_dirty_git_tree', return_value=True)
  2121. def test_patch_when_dirty(self, *_mocks):
  2122. # Patch when local tree is dirty.
  2123. self.assertNotEqual(git_cl.main(['patch', '123456']), 0)
  2124. def assertIssueAndPatchset(self,
  2125. branch='main',
  2126. issue='123456',
  2127. patchset='7',
  2128. git_short_host='chromium'):
  2129. self.assertEqual(issue,
  2130. scm.GIT.GetBranchConfig('', branch, 'gerritissue'))
  2131. self.assertEqual(patchset,
  2132. scm.GIT.GetBranchConfig('', branch, 'gerritpatchset'))
  2133. self.assertEqual('https://%s-review.googlesource.com' % git_short_host,
  2134. scm.GIT.GetBranchConfig('', branch, 'gerritserver'))
  2135. def _patch_common(self, git_short_host='chromium'):
  2136. mock.patch('scm.GIT.ResolveCommit', return_value='deadbeef').start()
  2137. scm.GIT.SetConfig('', 'remote.origin.url',
  2138. f'https://{git_short_host}.googlesource.com/my/repo')
  2139. gerrit_util.GetChangeDetail.return_value = {
  2140. 'current_revision': '7777777777',
  2141. 'revisions': {
  2142. '1111111111': {
  2143. '_number': 1,
  2144. 'fetch': {
  2145. 'http': {
  2146. 'url': 'https://%s.googlesource.com/my/repo' %
  2147. git_short_host,
  2148. 'ref': 'refs/changes/56/123456/1',
  2149. }
  2150. },
  2151. },
  2152. '7777777777': {
  2153. '_number': 7,
  2154. 'fetch': {
  2155. 'http': {
  2156. 'url': 'https://%s.googlesource.com/my/repo' %
  2157. git_short_host,
  2158. 'ref': 'refs/changes/56/123456/7',
  2159. }
  2160. },
  2161. },
  2162. },
  2163. }
  2164. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2165. 'not supported in non-git environment')
  2166. def test_patch_gerrit_default(self):
  2167. self._patch_common()
  2168. self.calls += [
  2169. (([
  2170. 'git', 'fetch', 'https://chromium.googlesource.com/my/repo',
  2171. 'refs/changes/56/123456/7'
  2172. ], ), ''),
  2173. ((['git', 'cherry-pick', 'FETCH_HEAD'], ), ''),
  2174. ]
  2175. self.assertEqual(git_cl.main(['patch', '123456']), 0)
  2176. self.assertIssueAndPatchset()
  2177. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2178. 'not supported in non-git environment')
  2179. def test_patch_gerrit_new_branch(self):
  2180. self._patch_common()
  2181. self.calls += [
  2182. (([
  2183. 'git', 'fetch', 'https://chromium.googlesource.com/my/repo',
  2184. 'refs/changes/56/123456/7'
  2185. ], ), ''),
  2186. ((['git', 'cherry-pick', 'FETCH_HEAD'], ), ''),
  2187. ]
  2188. self.assertEqual(git_cl.main(['patch', '-b', 'feature', '123456']), 0)
  2189. self.assertIssueAndPatchset(branch='feature')
  2190. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2191. 'not supported in non-git environment')
  2192. def test_patch_gerrit_force(self):
  2193. self._patch_common('host')
  2194. self.calls += [
  2195. (([
  2196. 'git', 'fetch', 'https://host.googlesource.com/my/repo',
  2197. 'refs/changes/56/123456/7'
  2198. ], ), ''),
  2199. ((['git', 'reset', '--hard', 'FETCH_HEAD'], ), ''),
  2200. ]
  2201. self.assertEqual(git_cl.main(['patch', '123456', '--force']), 0)
  2202. self.assertIssueAndPatchset(git_short_host='host')
  2203. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2204. 'not supported in non-git environment')
  2205. def test_patch_gerrit_guess_by_url(self):
  2206. self._patch_common('else')
  2207. self.calls += [
  2208. (([
  2209. 'git', 'fetch', 'https://else.googlesource.com/my/repo',
  2210. 'refs/changes/56/123456/1'
  2211. ], ), ''),
  2212. ((['git', 'cherry-pick', 'FETCH_HEAD'], ), ''),
  2213. ]
  2214. self.assertEqual(
  2215. git_cl.main(
  2216. ['patch', 'https://else-review.googlesource.com/#/c/123456/1']),
  2217. 0)
  2218. self.assertIssueAndPatchset(patchset='1', git_short_host='else')
  2219. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2220. 'not supported in non-git environment')
  2221. def test_patch_gerrit_guess_by_url_with_repo(self):
  2222. self._patch_common('else')
  2223. self.calls += [
  2224. (([
  2225. 'git', 'fetch', 'https://else.googlesource.com/my/repo',
  2226. 'refs/changes/56/123456/1'
  2227. ], ), ''),
  2228. ((['git', 'cherry-pick', 'FETCH_HEAD'], ), ''),
  2229. ]
  2230. self.assertEqual(
  2231. git_cl.main([
  2232. 'patch',
  2233. 'https://else-review.googlesource.com/c/my/repo/+/123456/1'
  2234. ]), 0)
  2235. self.assertIssueAndPatchset(patchset='1', git_short_host='else')
  2236. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2237. 'not supported in non-git environment')
  2238. @mock.patch('sys.stderr', io.StringIO())
  2239. def test_patch_gerrit_conflict(self):
  2240. self._patch_common()
  2241. self.calls += [
  2242. (([
  2243. 'git', 'fetch', 'https://chromium.googlesource.com/my/repo',
  2244. 'refs/changes/56/123456/7'
  2245. ], ), ''),
  2246. ((['git', 'cherry-pick', 'FETCH_HEAD'], ), CERR1),
  2247. ]
  2248. with self.assertRaises(SystemExitMock):
  2249. git_cl.main(['patch', '123456'])
  2250. self.assertEqual('Command "git cherry-pick FETCH_HEAD" failed.\n\n',
  2251. sys.stderr.getvalue())
  2252. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2253. 'not supported in non-git environment')
  2254. @mock.patch('gerrit_util.GetChangeDetail',
  2255. side_effect=gerrit_util.GerritError(404, ''))
  2256. @mock.patch('sys.stderr', io.StringIO())
  2257. def test_patch_gerrit_not_exists(self, *_mocks):
  2258. scm.GIT.SetConfig('', 'remote.origin.url',
  2259. 'https://chromium.googlesource.com/my/repo')
  2260. with self.assertRaises(SystemExitMock):
  2261. self.assertEqual(1, git_cl.main(['patch', '123456']))
  2262. self.assertEqual(
  2263. 'change 123456 at https://chromium-review.googlesource.com does not '
  2264. 'exist or you have no access to it\n', sys.stderr.getvalue())
  2265. def _checkout_config(self):
  2266. scm.GIT.SetConfig('', 'branch.ger-branch.gerritissue', '123456')
  2267. scm.GIT.SetConfig('', 'branch.gbranch654.gerritissue', '654321')
  2268. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2269. 'not supported in non-git environment')
  2270. def test_checkout_gerrit(self):
  2271. """Tests git cl checkout <issue>."""
  2272. self._checkout_config()
  2273. self.calls += [((['git', 'checkout', 'ger-branch'], ), '')]
  2274. self.assertEqual(0, git_cl.main(['checkout', '123456']))
  2275. def test_checkout_not_found(self):
  2276. """Tests git cl checkout <issue>."""
  2277. self._checkout_config()
  2278. self.assertEqual(1, git_cl.main(['checkout', '99999']))
  2279. def test_checkout_no_branch_issues(self):
  2280. """Tests git cl checkout <issue>."""
  2281. self.assertEqual(1, git_cl.main(['checkout', '99999']))
  2282. def _test_gerrit_ensure_authenticated_common(self, auth):
  2283. mock.patch(
  2284. 'gclient_utils.AskForData',
  2285. lambda prompt: self._mocked_call('ask_for_data', prompt)).start()
  2286. mock.patch(
  2287. 'git_cl.gerrit_util.CookiesAuthenticator',
  2288. CookiesAuthenticatorMockFactory(hosts_with_creds=auth)).start()
  2289. scm.GIT.SetConfig('', 'remote.origin.url',
  2290. 'https://chromium.googlesource.com/my/repo')
  2291. cl = git_cl.Changelist()
  2292. cl.branch = 'main'
  2293. cl.branchref = 'refs/heads/main'
  2294. return cl
  2295. @mock.patch('sys.stderr', io.StringIO())
  2296. def test_gerrit_ensure_authenticated_missing(self):
  2297. cl = self._test_gerrit_ensure_authenticated_common(auth={
  2298. 'chromium.googlesource.com': ('git-is.ok', 'but gerrit is missing'),
  2299. })
  2300. with self.assertRaises(SystemExitMock):
  2301. cl.EnsureAuthenticated(force=False)
  2302. self.assertEqual(
  2303. 'Credentials for the following hosts are required:\n'
  2304. ' chromium-review.googlesource.com\n'
  2305. 'These are read from ~%(sep)s.gitcookies\n'
  2306. 'You can (re)generate your credentials by visiting '
  2307. 'https://chromium.googlesource.com/new-password\n' % {
  2308. 'sep': os.sep,
  2309. }, sys.stderr.getvalue())
  2310. def test_gerrit_ensure_authenticated_conflict(self):
  2311. cl = self._test_gerrit_ensure_authenticated_common(
  2312. auth={
  2313. 'chromium.googlesource.com': ('git-one.example.com', 'secret1'),
  2314. 'chromium-review.googlesource.com': ('git-other.example.com',
  2315. 'secret2'),
  2316. })
  2317. self.calls.append((('ask_for_data', 'If you know what you are doing '
  2318. 'press Enter to continue, or Ctrl+C to abort'), ''))
  2319. self.assertIsNone(cl.EnsureAuthenticated(force=False))
  2320. def test_gerrit_ensure_authenticated_ok(self):
  2321. cl = self._test_gerrit_ensure_authenticated_common(
  2322. auth={
  2323. 'chromium.googlesource.com': ('git-same.example.com', 'secret'),
  2324. 'chromium-review.googlesource.com': ('git-same.example.com',
  2325. 'secret'),
  2326. })
  2327. self.assertIsNone(cl.EnsureAuthenticated(force=False))
  2328. def test_gerrit_ensure_authenticated_skipped(self):
  2329. scm.GIT.SetConfig('', 'gerrit.skip-ensure-authenticated', 'true')
  2330. cl = self._test_gerrit_ensure_authenticated_common(auth={})
  2331. self.assertIsNone(cl.EnsureAuthenticated(force=False))
  2332. def test_gerrit_ensure_authenticated_sso(self):
  2333. scm.GIT.SetConfig('', 'remote.origin.url', 'sso://repo')
  2334. mock.patch(
  2335. 'git_cl.gerrit_util.CookiesAuthenticator',
  2336. CookiesAuthenticatorMockFactory(hosts_with_creds={})).start()
  2337. cl = git_cl.Changelist()
  2338. cl.branch = 'main'
  2339. cl.branchref = 'refs/heads/main'
  2340. cl.lookedup_issue = True
  2341. self.assertIsNone(cl.EnsureAuthenticated(force=False))
  2342. def test_gerrit_ensure_authenticated_bearer_token(self):
  2343. cl = self._test_gerrit_ensure_authenticated_common(
  2344. auth={
  2345. 'chromium.googlesource.com': ('', 'secret'),
  2346. 'chromium-review.googlesource.com': ('', 'secret'),
  2347. })
  2348. self.assertIsNone(cl.EnsureAuthenticated(force=False))
  2349. conn = gerrit_util.HttpConn(
  2350. req_uri='???',
  2351. req_method='GET',
  2352. req_host='chromium.googlesource.com',
  2353. req_headers={},
  2354. req_body=None,
  2355. )
  2356. gerrit_util.CookiesAuthenticator().authenticate(conn)
  2357. self.assertTrue('Bearer' in conn.req_headers['Authorization'])
  2358. def test_gerrit_ensure_authenticated_non_https_sso(self):
  2359. scm.GIT.SetConfig('', 'remote.origin.url', 'custom-scheme://repo')
  2360. self.calls = [
  2361. (('logging.warning',
  2362. 'Ignoring branch %(branch)s with non-https remote '
  2363. '%(remote)s', {
  2364. 'branch': 'main',
  2365. 'remote': 'custom-scheme://repo'
  2366. }), None),
  2367. ]
  2368. mock.patch(
  2369. 'git_cl.gerrit_util.CookiesAuthenticator',
  2370. CookiesAuthenticatorMockFactory(hosts_with_creds={})).start()
  2371. mock.patch('logging.warning',
  2372. lambda *a: self._mocked_call('logging.warning', *a)).start()
  2373. cl = git_cl.Changelist()
  2374. cl.branch = 'main'
  2375. cl.branchref = 'refs/heads/main'
  2376. cl.lookedup_issue = True
  2377. self.assertIsNone(cl.EnsureAuthenticated(force=False))
  2378. def test_gerrit_ensure_authenticated_non_url(self):
  2379. scm.GIT.SetConfig('', 'remote.origin.url',
  2380. 'git@somehost.example:foo/bar.git')
  2381. self.calls = [
  2382. (('logging.error',
  2383. 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
  2384. 'but it doesn\'t exist.', {
  2385. 'remote': 'origin',
  2386. 'branch': 'main',
  2387. 'url': 'git@somehost.example:foo/bar.git'
  2388. }), None),
  2389. ]
  2390. mock.patch(
  2391. 'git_cl.gerrit_util.CookiesAuthenticator',
  2392. CookiesAuthenticatorMockFactory(hosts_with_creds={})).start()
  2393. mock.patch('logging.error',
  2394. lambda *a: self._mocked_call('logging.error', *a)).start()
  2395. cl = git_cl.Changelist()
  2396. cl.branch = 'main'
  2397. cl.branchref = 'refs/heads/main'
  2398. cl.lookedup_issue = True
  2399. self.assertIsNone(cl.EnsureAuthenticated(force=False))
  2400. def _cmd_set_commit_gerrit_common(self, vote, notify=None):
  2401. scm.GIT.SetConfig('', 'branch.main.gerritissue', '123')
  2402. scm.GIT.SetConfig('', 'branch.main.gerritserver',
  2403. 'https://chromium-review.googlesource.com')
  2404. scm.GIT.SetConfig('', 'remote.origin.url',
  2405. 'https://chromium.googlesource.com/infra/infra')
  2406. self.calls = [
  2407. (('SetReview', 'chromium-review.googlesource.com',
  2408. 'infra%2Finfra~123', None, {
  2409. 'Commit-Queue': vote
  2410. }, notify, None), ''),
  2411. ]
  2412. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2413. 'not supported in non-git environment')
  2414. def test_cmd_set_commit_gerrit_clear(self):
  2415. self._cmd_set_commit_gerrit_common(0)
  2416. self.assertEqual(0, git_cl.main(['set-commit', '-c']))
  2417. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2418. 'not supported in non-git environment')
  2419. def test_cmd_set_commit_gerrit_dry(self):
  2420. self._cmd_set_commit_gerrit_common(1, notify=False)
  2421. self.assertEqual(0, git_cl.main(['set-commit', '-d']))
  2422. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2423. 'not supported in non-git environment')
  2424. def test_cmd_set_commit_gerrit(self):
  2425. self._cmd_set_commit_gerrit_common(2)
  2426. self.assertEqual(0, git_cl.main(['set-commit']))
  2427. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2428. 'not supported in non-git environment')
  2429. def test_description_display(self):
  2430. mock.patch('git_cl.Changelist', ChangelistMock).start()
  2431. ChangelistMock.desc = 'foo\n'
  2432. self.assertEqual(0, git_cl.main(['description', '-d']))
  2433. self.assertEqual('foo\n', sys.stdout.getvalue())
  2434. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2435. 'not supported in non-git environment')
  2436. @mock.patch('sys.stderr', io.StringIO())
  2437. def test_StatusFieldOverrideIssueMissingArgs(self):
  2438. try:
  2439. self.assertEqual(git_cl.main(['status', '--issue', '1']), 0)
  2440. except SystemExitMock:
  2441. self.assertIn('--field must be given when --issue is set.',
  2442. sys.stderr.getvalue())
  2443. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2444. 'not supported in non-git environment')
  2445. def test_StatusFieldOverrideIssue(self):
  2446. def assertIssue(cl_self, *_args):
  2447. self.assertEqual(cl_self.issue, 1)
  2448. return 'foobar'
  2449. mock.patch('git_cl.Changelist.FetchDescription', assertIssue).start()
  2450. self.assertEqual(
  2451. git_cl.main(['status', '--issue', '1', '--field', 'desc']), 0)
  2452. self.assertEqual(sys.stdout.getvalue(), 'foobar\n')
  2453. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2454. 'not supported in non-git environment')
  2455. def test_SetCloseOverrideIssue(self):
  2456. def assertIssue(cl_self, *_args):
  2457. self.assertEqual(cl_self.issue, 1)
  2458. return 'foobar'
  2459. mock.patch('git_cl.Changelist.FetchDescription', assertIssue).start()
  2460. mock.patch('git_cl.Changelist.CloseIssue', lambda *_: None).start()
  2461. self.assertEqual(git_cl.main(['set-close', '--issue', '1']), 0)
  2462. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2463. 'not supported in non-git environment')
  2464. def test_description(self):
  2465. scm.GIT.SetConfig('', 'remote.origin.url',
  2466. 'https://chromium.googlesource.com/my/repo')
  2467. gerrit_util.GetChangeDetail.return_value = {
  2468. 'current_revision': 'sha1',
  2469. 'revisions': {
  2470. 'sha1': {
  2471. 'commit': {
  2472. 'message': 'foobar'
  2473. },
  2474. }
  2475. },
  2476. }
  2477. self.assertEqual(
  2478. 0,
  2479. git_cl.main([
  2480. 'description',
  2481. 'https://chromium-review.googlesource.com/c/my/repo/+/123123',
  2482. '-d'
  2483. ]))
  2484. self.assertEqual('foobar\n', sys.stdout.getvalue())
  2485. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2486. 'not supported in non-git environment')
  2487. def test_description_set_raw(self):
  2488. mock.patch('git_cl.Changelist', ChangelistMock).start()
  2489. mock.patch('git_cl.sys.stdin', io.StringIO('hihi')).start()
  2490. self.assertEqual(0, git_cl.main(['description', '-n', 'hihi']))
  2491. self.assertEqual('hihi', ChangelistMock.desc)
  2492. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2493. 'not supported in non-git environment')
  2494. def test_description_appends_bug_line(self):
  2495. current_desc = 'Some.\n\nChange-Id: xxx'
  2496. def RunEditor(desc, _, **kwargs):
  2497. self.assertEqual(
  2498. '# Enter a description of the change.\n'
  2499. '# This will be displayed on the codereview site.\n'
  2500. '# The first line will also be used as the subject of the review.\n'
  2501. '#--------------------This line is 72 characters long'
  2502. '--------------------\n'
  2503. 'Some.\n\nChange-Id: xxx\nBug: ', desc)
  2504. # Simulate user changing something.
  2505. return 'Some.\n\nChange-Id: xxx\nBug: 123'
  2506. def UpdateDescription(_, desc, force=False):
  2507. self.assertEqual(desc, 'Some.\n\nChange-Id: xxx\nBug: 123')
  2508. mock.patch('git_cl.Changelist.FetchDescription',
  2509. lambda *args: current_desc).start()
  2510. mock.patch('git_cl.Changelist.UpdateDescription',
  2511. UpdateDescription).start()
  2512. mock.patch('git_cl.gclient_utils.RunEditor', RunEditor).start()
  2513. scm.GIT.SetConfig('', 'branch.main.gerritissue', '123')
  2514. self.assertEqual(0, git_cl.main(['description']))
  2515. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2516. 'not supported in non-git environment')
  2517. def test_description_does_not_append_bug_line_if_fixed_is_present(self):
  2518. current_desc = 'Some.\n\nFixed: 123\nChange-Id: xxx'
  2519. def RunEditor(desc, _, **kwargs):
  2520. self.assertEqual(
  2521. '# Enter a description of the change.\n'
  2522. '# This will be displayed on the codereview site.\n'
  2523. '# The first line will also be used as the subject of the review.\n'
  2524. '#--------------------This line is 72 characters long'
  2525. '--------------------\n'
  2526. 'Some.\n\nFixed: 123\nChange-Id: xxx', desc)
  2527. return desc
  2528. mock.patch('git_cl.Changelist.FetchDescription',
  2529. lambda *args: current_desc).start()
  2530. mock.patch('git_cl.gclient_utils.RunEditor', RunEditor).start()
  2531. scm.GIT.SetConfig('', 'branch.main.gerritissue', '123')
  2532. self.assertEqual(0, git_cl.main(['description']))
  2533. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2534. 'not supported in non-git environment')
  2535. def test_description_set_stdin(self):
  2536. mock.patch('git_cl.Changelist', ChangelistMock).start()
  2537. mock.patch('git_cl.sys.stdin',
  2538. io.StringIO('hi \r\n\t there\n\nman')).start()
  2539. self.assertEqual(0, git_cl.main(['description', '-n', '-']))
  2540. self.assertEqual('hi\n\t there\n\nman', ChangelistMock.desc)
  2541. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2542. 'not supported in non-git environment')
  2543. def test_archive(self):
  2544. self.calls = [
  2545. ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
  2546. 'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
  2547. ((['git', 'for-each-ref', '--format=%(refname)',
  2548. 'refs/tags'], ), ''),
  2549. ((['git', 'tag', 'git-cl-archived-456-foo', 'foo'], ), ''),
  2550. ((['git', 'branch', '-D', 'foo'], ), '')
  2551. ]
  2552. mock.patch(
  2553. 'git_cl.get_cl_statuses',
  2554. lambda branches, fine_grained, max_processes: [
  2555. (MockChangelistWithBranchAndIssue('main', 1), 'open'),
  2556. (MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
  2557. (MockChangelistWithBranchAndIssue('bar', 789), 'open')
  2558. ]).start()
  2559. self.assertEqual(0, git_cl.main(['archive', '-f']))
  2560. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2561. 'not supported in non-git environment')
  2562. def test_archive_tag_collision(self):
  2563. self.calls = [
  2564. ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
  2565. 'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
  2566. ((['git', 'for-each-ref', '--format=%(refname)',
  2567. 'refs/tags'], ), 'refs/tags/git-cl-archived-456-foo'),
  2568. ((['git', 'tag', 'git-cl-archived-456-foo-2', 'foo'], ), ''),
  2569. ((['git', 'branch', '-D', 'foo'], ), '')
  2570. ]
  2571. mock.patch(
  2572. 'git_cl.get_cl_statuses',
  2573. lambda branches, fine_grained, max_processes: [
  2574. (MockChangelistWithBranchAndIssue('main', 1), 'open'),
  2575. (MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
  2576. (MockChangelistWithBranchAndIssue('bar', 789), 'open')
  2577. ]).start()
  2578. self.assertEqual(0, git_cl.main(['archive', '-f']))
  2579. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2580. 'not supported in non-git environment')
  2581. def test_archive_current_branch_fails(self):
  2582. self.calls = [
  2583. ((['git', 'for-each-ref', '--format=%(refname)',
  2584. 'refs/heads'], ), 'refs/heads/main'),
  2585. ((['git', 'for-each-ref', '--format=%(refname)',
  2586. 'refs/tags'], ), ''),
  2587. ]
  2588. mock.patch(
  2589. 'git_cl.get_cl_statuses',
  2590. lambda branches, fine_grained, max_processes: [
  2591. (MockChangelistWithBranchAndIssue('main', 1), 'closed')
  2592. ]).start()
  2593. self.assertEqual(1, git_cl.main(['archive', '-f']))
  2594. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2595. 'not supported in non-git environment')
  2596. def test_archive_dry_run(self):
  2597. self.calls = [
  2598. ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
  2599. 'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
  2600. ((['git', 'for-each-ref', '--format=%(refname)',
  2601. 'refs/tags'], ), ''),
  2602. ]
  2603. mock.patch(
  2604. 'git_cl.get_cl_statuses',
  2605. lambda branches, fine_grained, max_processes: [
  2606. (MockChangelistWithBranchAndIssue('main', 1), 'open'),
  2607. (MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
  2608. (MockChangelistWithBranchAndIssue('bar', 789), 'open')
  2609. ]).start()
  2610. self.assertEqual(0, git_cl.main(['archive', '-f', '--dry-run']))
  2611. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2612. 'not supported in non-git environment')
  2613. def test_archive_no_tags(self):
  2614. self.calls = [
  2615. ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
  2616. 'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
  2617. ((['git', 'for-each-ref', '--format=%(refname)',
  2618. 'refs/tags'], ), ''), ((['git', 'branch', '-D', 'foo'], ), '')
  2619. ]
  2620. mock.patch(
  2621. 'git_cl.get_cl_statuses',
  2622. lambda branches, fine_grained, max_processes: [
  2623. (MockChangelistWithBranchAndIssue('main', 1), 'open'),
  2624. (MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
  2625. (MockChangelistWithBranchAndIssue('bar', 789), 'open')
  2626. ]).start()
  2627. self.assertEqual(0, git_cl.main(['archive', '-f', '--notags']))
  2628. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2629. 'not supported in non-git environment')
  2630. def test_archive_tag_cleanup_on_branch_deletion_error(self):
  2631. self.calls = [
  2632. ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
  2633. 'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
  2634. ((['git', 'for-each-ref', '--format=%(refname)',
  2635. 'refs/tags'], ), ''),
  2636. ((['git', 'tag', 'git-cl-archived-456-foo',
  2637. 'foo'], ), 'refs/tags/git-cl-archived-456-foo'),
  2638. ((['git', 'branch', '-D', 'foo'], ), CERR1),
  2639. ((['git', 'tag', '-d', 'git-cl-archived-456-foo'], ),
  2640. 'refs/tags/git-cl-archived-456-foo'),
  2641. ]
  2642. mock.patch(
  2643. 'git_cl.get_cl_statuses',
  2644. lambda branches, fine_grained, max_processes: [
  2645. (MockChangelistWithBranchAndIssue('main', 1), 'open'),
  2646. (MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
  2647. (MockChangelistWithBranchAndIssue('bar', 789), 'open')
  2648. ]).start()
  2649. self.assertEqual(0, git_cl.main(['archive', '-f']))
  2650. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2651. 'not supported in non-git environment')
  2652. def test_archive_with_format(self):
  2653. self.calls = [
  2654. ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
  2655. 'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
  2656. ((['git', 'for-each-ref', '--format=%(refname)',
  2657. 'refs/tags'], ), ''),
  2658. ((['git', 'tag', 'archived/12-foo', 'foo'], ), ''),
  2659. ((['git', 'branch', '-D', 'foo'], ), ''),
  2660. ]
  2661. mock.patch(
  2662. 'git_cl.get_cl_statuses',
  2663. lambda branches, fine_grained, max_processes: [
  2664. (MockChangelistWithBranchAndIssue('foo', 12), 'closed')
  2665. ]).start()
  2666. self.assertEqual(
  2667. 0, git_cl.main(['archive', '-f', '-p',
  2668. 'archived/{issue}-{branch}']))
  2669. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2670. 'not supported in non-git environment')
  2671. def test_cmd_issue_erase_existing(self):
  2672. scm.GIT.SetConfig('', 'branch.main.gerritissue', '123')
  2673. scm.GIT.SetConfig('', 'branch.main.gerritserver',
  2674. 'https://chromium-review.googlesource.com')
  2675. self.calls = [
  2676. ((['git', 'log', '-1', '--format=%B'], ), 'This is a description'),
  2677. ]
  2678. self.assertEqual(0, git_cl.main(['issue', '0']))
  2679. self.assertIsNone(scm.GIT.GetConfig('root', 'branch.main.gerritissue'))
  2680. self.assertIsNone(scm.GIT.GetConfig('root', 'branch.main.gerritserver'))
  2681. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2682. 'not supported in non-git environment')
  2683. def test_cmd_issue_erase_existing_with_change_id(self):
  2684. scm.GIT.SetConfig('', 'branch.main.gerritissue', '123')
  2685. scm.GIT.SetConfig('', 'branch.main.gerritserver',
  2686. 'https://chromium-review.googlesource.com')
  2687. mock.patch(
  2688. 'git_cl.Changelist.FetchDescription',
  2689. lambda _: 'This is a description\n\nChange-Id: Ideadbeef').start()
  2690. self.calls = [
  2691. ((['git', 'log', '-1', '--format=%B'], ),
  2692. 'This is a description\n\nChange-Id: Ideadbeef'),
  2693. ((['git', 'commit', '--amend', '-m',
  2694. 'This is a description\n'], ), ''),
  2695. ]
  2696. self.assertEqual(0, git_cl.main(['issue', '0']))
  2697. self.assertIsNone(scm.GIT.GetConfig('root', 'branch.main.gerritissue'))
  2698. self.assertIsNone(scm.GIT.GetConfig('root', 'branch.main.gerritserver'))
  2699. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2700. 'not supported in non-git environment')
  2701. def test_cmd_issue_json(self):
  2702. scm.GIT.SetConfig('', 'branch.main.gerritissue', '123')
  2703. scm.GIT.SetConfig('', 'branch.main.gerritserver',
  2704. 'https://chromium-review.googlesource.com')
  2705. scm.GIT.SetConfig('', 'remote.origin.url',
  2706. 'https://chromium.googlesource.com/chromium/src')
  2707. self.calls = [(
  2708. (
  2709. 'write_json',
  2710. 'output.json',
  2711. {
  2712. 'issue': 123,
  2713. 'issue_url': 'https://chromium-review.googlesource.com/123',
  2714. 'gerrit_host': 'chromium-review.googlesource.com',
  2715. 'gerrit_project': 'chromium/src',
  2716. },
  2717. ),
  2718. '',
  2719. )]
  2720. self.assertEqual(0, git_cl.main(['issue', '--json', 'output.json']))
  2721. def _common_GerritCommitMsgHookCheck(self):
  2722. mock.patch('git_cl.os.path.abspath',
  2723. lambda path: self._mocked_call(['abspath', path])).start()
  2724. mock.patch('git_cl.os.path.exists',
  2725. lambda path: self._mocked_call(['exists', path])).start()
  2726. mock.patch('git_cl.gclient_utils.FileRead',
  2727. lambda path: self._mocked_call(['FileRead', path])).start()
  2728. mock.patch(
  2729. 'git_cl.gclient_utils.rm_file_or_tree',
  2730. lambda path: self._mocked_call(['rm_file_or_tree', path])).start()
  2731. mock.patch(
  2732. 'gclient_utils.AskForData',
  2733. lambda prompt: self._mocked_call('ask_for_data', prompt)).start()
  2734. return git_cl.Changelist(issue=123)
  2735. def test_GerritCommitMsgHookCheck_custom_hook(self):
  2736. cl = self._common_GerritCommitMsgHookCheck()
  2737. self.calls += [
  2738. ((['exists', os.path.join('.git', 'hooks', 'commit-msg')], ), True),
  2739. ((['FileRead',
  2740. os.path.join('.git', 'hooks',
  2741. 'commit-msg')], ), '#!/bin/sh\necho "custom hook"')
  2742. ]
  2743. cl._GerritCommitMsgHookCheck(offer_removal=True)
  2744. def test_GerritCommitMsgHookCheck_not_exists(self):
  2745. cl = self._common_GerritCommitMsgHookCheck()
  2746. self.calls += [
  2747. ((['exists', os.path.join('.git', 'hooks',
  2748. 'commit-msg')], ), False),
  2749. ]
  2750. cl._GerritCommitMsgHookCheck(offer_removal=True)
  2751. def test_GerritCommitMsgHookCheck(self):
  2752. cl = self._common_GerritCommitMsgHookCheck()
  2753. self.calls += [
  2754. ((['exists', os.path.join('.git', 'hooks', 'commit-msg')], ), True),
  2755. ((['FileRead',
  2756. os.path.join('.git', 'hooks', 'commit-msg')], ),
  2757. '...\n# From Gerrit Code Review\n...\nadd_ChangeId()\n'),
  2758. (('ask_for_data', 'Do you want to remove it now? [Yes/No]: '),
  2759. 'Yes'),
  2760. ((['rm_file_or_tree',
  2761. os.path.join('.git', 'hooks', 'commit-msg')], ), ''),
  2762. ]
  2763. cl._GerritCommitMsgHookCheck(offer_removal=True)
  2764. def test_GerritCmdLand(self):
  2765. scm.GIT.SetConfig('', 'branch.main.gerritsquashhash', 'deadbeaf')
  2766. scm.GIT.SetConfig('', 'branch.main.gerritserver',
  2767. 'chromium-review.googlesource.com')
  2768. self.calls += [
  2769. ((['git', 'diff', 'deadbeaf'], ), ''), # No diff.
  2770. ]
  2771. cl = git_cl.Changelist(issue=123)
  2772. cl._GetChangeDetail = lambda *args, **kwargs: {
  2773. 'labels': {},
  2774. 'current_revision': 'deadbeaf',
  2775. }
  2776. cl._GetChangeCommit = lambda: {
  2777. 'commit':
  2778. 'deadbeef',
  2779. 'web_links': [{
  2780. 'name': 'gitiles',
  2781. 'url': 'https://git.googlesource.com/test/+/deadbeef'
  2782. }],
  2783. }
  2784. cl.SubmitIssue = lambda: None
  2785. self.assertEqual(
  2786. 0,
  2787. cl.CMDLand(force=True,
  2788. bypass_hooks=True,
  2789. verbose=True,
  2790. parallel=False,
  2791. resultdb=False,
  2792. realm=None))
  2793. self.assertIn(
  2794. 'Issue chromium-review.googlesource.com/123 has been submitted',
  2795. sys.stdout.getvalue())
  2796. self.assertIn('Landed as: https://git.googlesource.com/test/+/deadbeef',
  2797. sys.stdout.getvalue())
  2798. def _mock_gerrit_changes_for_detail_cache(self):
  2799. mock.patch('git_cl.Changelist.GetGerritHost', lambda _: 'host').start()
  2800. def test_gerrit_change_detail_cache_simple(self):
  2801. self._mock_gerrit_changes_for_detail_cache()
  2802. gerrit_util.GetChangeDetail.side_effect = ['a', 'b']
  2803. cl1 = git_cl.Changelist(issue=1)
  2804. cl1._cached_remote_url = (
  2805. True, 'https://chromium.googlesource.com/a/my/repo.git/')
  2806. cl2 = git_cl.Changelist(issue=2)
  2807. cl2._cached_remote_url = (True,
  2808. 'https://chromium.googlesource.com/ab/repo')
  2809. self.assertEqual(cl1._GetChangeDetail(), 'a') # Miss.
  2810. self.assertEqual(cl1._GetChangeDetail(), 'a')
  2811. self.assertEqual(cl2._GetChangeDetail(), 'b') # Miss.
  2812. def test_gerrit_change_detail_cache_options(self):
  2813. self._mock_gerrit_changes_for_detail_cache()
  2814. gerrit_util.GetChangeDetail.side_effect = ['cab', 'ad']
  2815. cl = git_cl.Changelist(issue=1)
  2816. cl._cached_remote_url = (True,
  2817. 'https://chromium.googlesource.com/repo/')
  2818. self.assertEqual(cl._GetChangeDetail(options=['C', 'A', 'B']), 'cab')
  2819. self.assertEqual(cl._GetChangeDetail(options=['A', 'B', 'C']), 'cab')
  2820. self.assertEqual(cl._GetChangeDetail(options=['B', 'A']), 'cab')
  2821. self.assertEqual(cl._GetChangeDetail(options=['C']), 'cab')
  2822. self.assertEqual(cl._GetChangeDetail(options=['A']), 'cab')
  2823. self.assertEqual(cl._GetChangeDetail(), 'cab')
  2824. self.assertEqual(cl._GetChangeDetail(options=['A', 'D']), 'ad')
  2825. self.assertEqual(cl._GetChangeDetail(options=['A']), 'cab')
  2826. self.assertEqual(cl._GetChangeDetail(options=['D']), 'ad')
  2827. self.assertEqual(cl._GetChangeDetail(), 'cab')
  2828. def test_gerrit_description_caching(self):
  2829. gerrit_util.GetChangeDetail.return_value = {
  2830. 'current_revision': 'rev1',
  2831. 'revisions': {
  2832. 'rev1': {
  2833. 'commit': {
  2834. 'message': 'desc1'
  2835. }
  2836. },
  2837. },
  2838. }
  2839. self._mock_gerrit_changes_for_detail_cache()
  2840. cl = git_cl.Changelist(issue=1)
  2841. cl._cached_remote_url = (
  2842. True, 'https://chromium.googlesource.com/a/my/repo.git/')
  2843. self.assertEqual(cl.FetchDescription(), 'desc1')
  2844. self.assertEqual(cl.FetchDescription(), 'desc1') # cache hit.
  2845. def test_print_current_creds(self):
  2846. class CookiesAuthenticatorMock(object):
  2847. def __init__(self):
  2848. self.gitcookies = {
  2849. 'host.googlesource.com': ('user', 'pass'),
  2850. 'host-review.googlesource.com': ('user', 'pass'),
  2851. }
  2852. mock.patch('git_cl.gerrit_util.CookiesAuthenticator',
  2853. CookiesAuthenticatorMock).start()
  2854. git_cl._GitCookiesChecker().print_current_creds()
  2855. self.assertEqual(list(sys.stdout.getvalue().splitlines()), [
  2856. 'Your .gitcookies have credentials for these hosts:',
  2857. ' Host\tUser\t Which file',
  2858. '============================\t====\t===========',
  2859. 'host-review.googlesource.com\tuser\t.gitcookies',
  2860. ' host.googlesource.com\tuser\t.gitcookies',
  2861. ])
  2862. sys.stdout.seek(0)
  2863. sys.stdout.truncate(0)
  2864. git_cl._GitCookiesChecker().print_current_creds()
  2865. self.assertEqual(list(sys.stdout.getvalue().splitlines()), [
  2866. 'Your .gitcookies have credentials for these hosts:',
  2867. ' Host\tUser\t Which file',
  2868. '============================\t====\t===========',
  2869. 'host-review.googlesource.com\tuser\t.gitcookies',
  2870. ' host.googlesource.com\tuser\t.gitcookies',
  2871. ])
  2872. def _common_creds_check_mocks(self):
  2873. def exists_mock(path):
  2874. dirname = os.path.dirname(path)
  2875. if dirname == os.path.expanduser('~'):
  2876. dirname = '~'
  2877. base = os.path.basename(path)
  2878. if base == '.gitcookies':
  2879. return self._mocked_call('os.path.exists',
  2880. os.path.join(dirname, base))
  2881. # git cl also checks for existence other files not relevant to this
  2882. # test.
  2883. return None
  2884. mock.patch(
  2885. 'gclient_utils.AskForData',
  2886. lambda prompt: self._mocked_call('ask_for_data', prompt)).start()
  2887. mock.patch('os.path.exists', exists_mock).start()
  2888. def test_creds_check_gitcookies_not_configured(self):
  2889. self._common_creds_check_mocks()
  2890. mock.patch('git_cl._GitCookiesChecker.get_hosts_with_creds',
  2891. lambda _: []).start()
  2892. self.calls = [
  2893. (('ask_for_data', 'Press Enter to setup .gitcookies, '
  2894. 'or Ctrl+C to abort'), ''),
  2895. ]
  2896. self.assertEqual(0, git_cl.main(['creds-check']))
  2897. self.assertIn('\nConfigured git to use .gitcookies from',
  2898. sys.stdout.getvalue())
  2899. def test_creds_check_gitcookies_configured_custom_broken(self):
  2900. self._common_creds_check_mocks()
  2901. custom_cookie_path = ('C:\\.gitcookies' if sys.platform == 'win32' else
  2902. '/custom/.gitcookies')
  2903. scm.GIT.SetConfig('', 'http.cookiefile', custom_cookie_path)
  2904. os.environ['GIT_COOKIES_PATH'] = '/official/.gitcookies'
  2905. mock.patch('git_cl._GitCookiesChecker.get_hosts_with_creds',
  2906. lambda _: []).start()
  2907. self.calls = [
  2908. (('os.path.exists', custom_cookie_path), False),
  2909. (('ask_for_data', 'Reconfigure git to use default .gitcookies? '
  2910. 'Press Enter to reconfigure, or Ctrl+C to abort'), ''),
  2911. ]
  2912. self.assertEqual(0, git_cl.main(['creds-check']))
  2913. self.assertIn(
  2914. 'WARNING: You have configured custom path to .gitcookies: ',
  2915. sys.stdout.getvalue())
  2916. self.assertIn('However, your configured .gitcookies file is missing.',
  2917. sys.stdout.getvalue())
  2918. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2919. 'not supported in non-git environment')
  2920. def test_git_cl_comment_add_gerrit(self):
  2921. git_new_branch.create_new_branch(None) # hits mock from scm_mock.GIT.
  2922. scm.GIT.SetConfig('', 'remote.origin.url',
  2923. 'https://chromium.googlesource.com/infra/infra')
  2924. self.calls = [
  2925. (('SetReview', 'chromium-review.googlesource.com',
  2926. 'infra%2Finfra~10', 'msg', None, None, None), None),
  2927. ]
  2928. self.assertEqual(0, git_cl.main(['comment', '-i', '10', '-a', 'msg']))
  2929. @unittest.skipIf(gclient_utils.IsEnvCog(),
  2930. 'not supported in non-git environment')
  2931. @mock.patch('git_cl.Changelist.GetBranch', return_value='foo')
  2932. def test_git_cl_comments_fetch_gerrit(self, *_mocks):
  2933. scm.GIT.SetConfig('', 'remote.origin.url',
  2934. 'https://chromium.googlesource.com/infra/infra')
  2935. gerrit_util.GetChangeDetail.return_value = {
  2936. 'owner': {
  2937. 'email': 'owner@example.com'
  2938. },
  2939. 'current_revision':
  2940. 'ba5eba11',
  2941. 'revisions': {
  2942. 'deadbeaf': {
  2943. '_number': 1,
  2944. },
  2945. 'ba5eba11': {
  2946. '_number': 2,
  2947. },
  2948. },
  2949. 'messages': [
  2950. {
  2951. u'_revision_number': 1,
  2952. u'author': {
  2953. u'_account_id': 1111084,
  2954. u'email': u'could-be-anything@example.com',
  2955. u'name': u'LUCI CQ'
  2956. },
  2957. u'date': u'2017-03-15 20:08:45.000000000',
  2958. u'id': u'f5a6c25ecbd3b3b54a43ae418ed97eff046dc50b',
  2959. u'message':
  2960. u'Patch Set 1:\n\nDry run: CQ is trying the patch...',
  2961. u'tag': u'autogenerated:cv:dry-run'
  2962. },
  2963. {
  2964. u'_revision_number': 2,
  2965. u'author': {
  2966. u'_account_id': 11151243,
  2967. u'email': u'owner@example.com',
  2968. u'name': u'owner'
  2969. },
  2970. u'date': u'2017-03-16 20:00:41.000000000',
  2971. u'id': u'f5a6c25ecbd3b3b54a43ae418ed97eff046d1234',
  2972. u'message': u'PTAL',
  2973. },
  2974. {
  2975. u'_revision_number': 2,
  2976. u'author': {
  2977. u'_account_id': 148512,
  2978. u'email': u'reviewer@example.com',
  2979. u'name': u'reviewer'
  2980. },
  2981. u'date': u'2017-03-17 05:19:37.500000000',
  2982. u'id': u'f5a6c25ecbd3b3b54a43ae418ed97eff046d4568',
  2983. u'message': u'Patch Set 2: Code-Review+1',
  2984. },
  2985. {
  2986. u'_revision_number': 2,
  2987. u'author': {
  2988. u'_account_id': 42,
  2989. u'name': u'reviewer'
  2990. },
  2991. u'date': u'2017-03-17 05:19:37.900000000',
  2992. u'id': u'f5a6c25ecbd3b3b54a43ae418ed97eff046d0000',
  2993. u'message': u'A bot with no email set',
  2994. },
  2995. ]
  2996. }
  2997. self.calls = [
  2998. (('GetChangeComments', 'chromium-review.googlesource.com',
  2999. 'infra%2Finfra~1'), {
  3000. '/COMMIT_MSG': [
  3001. {
  3002. 'author': {
  3003. 'email': u'reviewer@example.com'
  3004. },
  3005. 'updated': u'2017-03-17 05:19:37.500000000',
  3006. 'patch_set': 2,
  3007. 'side': 'REVISION',
  3008. 'message': 'Please include a bug link',
  3009. },
  3010. ],
  3011. 'codereview.settings': [
  3012. {
  3013. 'author': {
  3014. 'email': u'owner@example.com'
  3015. },
  3016. 'updated': u'2017-03-16 20:00:41.000000000',
  3017. 'patch_set': 2,
  3018. 'side': 'PARENT',
  3019. 'line': 42,
  3020. 'message': 'I removed this because it is bad',
  3021. },
  3022. ]
  3023. }),
  3024. (('GetChangeRobotComments', 'chromium-review.googlesource.com',
  3025. 'infra%2Finfra~1'), {}),
  3026. ] * 2 + [(('write_json', 'output.json', [{
  3027. u'date':
  3028. u'2017-03-16 20:00:41.000000',
  3029. u'message': (u'PTAL\n' + u'\n' + u'codereview.settings\n' +
  3030. u' Base, Line 42: https://crrev.com/c/1/2/'
  3031. u'codereview.settings#b42\n' +
  3032. u' I removed this because it is bad\n'),
  3033. u'autogenerated':
  3034. False,
  3035. u'approval':
  3036. False,
  3037. u'disapproval':
  3038. False,
  3039. u'sender':
  3040. u'owner@example.com'
  3041. }, {
  3042. u'date':
  3043. u'2017-03-17 05:19:37.500000',
  3044. u'message':
  3045. (u'Patch Set 2: Code-Review+1\n' + u'\n' + u'/COMMIT_MSG\n' +
  3046. u' PS2, File comment: https://crrev.com/c/1/2//COMMIT_MSG#\n' +
  3047. u' Please include a bug link\n'),
  3048. u'autogenerated':
  3049. False,
  3050. u'approval':
  3051. False,
  3052. u'disapproval':
  3053. False,
  3054. u'sender':
  3055. u'reviewer@example.com'
  3056. }]), '')]
  3057. expected_comments_summary = [
  3058. git_cl._CommentSummary(
  3059. message=(u'PTAL\n' + u'\n' + u'codereview.settings\n' +
  3060. u' Base, Line 42: https://crrev.com/c/1/2/' +
  3061. u'codereview.settings#b42\n' +
  3062. u' I removed this because it is bad\n'),
  3063. date=datetime.datetime(2017, 3, 16, 20, 0, 41, 0),
  3064. autogenerated=False,
  3065. disapproval=False,
  3066. approval=False,
  3067. sender=u'owner@example.com'),
  3068. git_cl._CommentSummary(message=(
  3069. u'Patch Set 2: Code-Review+1\n' + u'\n' + u'/COMMIT_MSG\n' +
  3070. u' PS2, File comment: https://crrev.com/c/1/2//COMMIT_MSG#\n' +
  3071. u' Please include a bug link\n'),
  3072. date=datetime.datetime(
  3073. 2017, 3, 17, 5, 19, 37, 500000),
  3074. autogenerated=False,
  3075. disapproval=False,
  3076. approval=False,
  3077. sender=u'reviewer@example.com'),
  3078. ]
  3079. cl = git_cl.Changelist(issue=1, branchref='refs/heads/foo')
  3080. self.assertEqual(cl.GetCommentsSummary(), expected_comments_summary)
  3081. self.assertEqual(
  3082. 0, git_cl.main(['comments', '-i', '1', '-j', 'output.json']))
  3083. @unittest.skipIf(gclient_utils.IsEnvCog(),
  3084. 'not supported in non-git environment')
  3085. def test_git_cl_comments_robot_comments(self):
  3086. # git cl comments also fetches robot comments (which are considered a
  3087. # type of autogenerated comment), and unlike other types of comments,
  3088. # only robot comments from the latest patchset are shown.
  3089. scm.GIT.SetConfig('', 'remote.origin.url',
  3090. 'https://x.googlesource.com/infra/infra')
  3091. gerrit_util.GetChangeDetail.return_value = {
  3092. 'owner': {
  3093. 'email': 'owner@example.com'
  3094. },
  3095. 'current_revision':
  3096. 'ba5eba11',
  3097. 'revisions': {
  3098. 'deadbeaf': {
  3099. '_number': 1,
  3100. },
  3101. 'ba5eba11': {
  3102. '_number': 2,
  3103. },
  3104. },
  3105. 'messages': [
  3106. {
  3107. u'_revision_number': 1,
  3108. u'author': {
  3109. u'_account_id': 1111084,
  3110. u'email': u'commit-bot@chromium.org',
  3111. u'name': u'Commit Bot'
  3112. },
  3113. u'date': u'2017-03-15 20:08:45.000000000',
  3114. u'id': u'f5a6c25ecbd3b3b54a43ae418ed97eff046dc50b',
  3115. u'message':
  3116. u'Patch Set 1:\n\nDry run: CQ is trying the patch...',
  3117. u'tag': u'autogenerated:cq:dry-run'
  3118. },
  3119. {
  3120. u'_revision_number': 1,
  3121. u'author': {
  3122. u'_account_id': 123,
  3123. u'email': u'tricium@serviceaccount.com',
  3124. u'name': u'Tricium'
  3125. },
  3126. u'date': u'2017-03-16 20:00:41.000000000',
  3127. u'id': u'f5a6c25ecbd3b3b54a43ae418ed97eff046d1234',
  3128. u'message': u'(1 comment)',
  3129. u'tag': u'autogenerated:tricium',
  3130. },
  3131. {
  3132. u'_revision_number': 1,
  3133. u'author': {
  3134. u'_account_id': 123,
  3135. u'email': u'tricium@serviceaccount.com',
  3136. u'name': u'Tricium'
  3137. },
  3138. u'date': u'2017-03-16 20:00:41.000000000',
  3139. u'id': u'f5a6c25ecbd3b3b54a43ae418ed97eff046d1234',
  3140. u'message': u'(1 comment)',
  3141. u'tag': u'autogenerated:tricium',
  3142. },
  3143. {
  3144. u'_revision_number': 2,
  3145. u'author': {
  3146. u'_account_id': 123,
  3147. u'email': u'tricium@serviceaccount.com',
  3148. u'name': u'reviewer'
  3149. },
  3150. u'date': u'2017-03-17 05:30:37.000000000',
  3151. u'tag': u'autogenerated:tricium',
  3152. u'id': u'f5a6c25ecbd3b3b54a43ae418ed97eff046d4568',
  3153. u'message': u'(1 comment)',
  3154. },
  3155. ]
  3156. }
  3157. self.calls = [
  3158. (('GetChangeComments', 'x-review.googlesource.com',
  3159. 'infra%2Finfra~1'), {}),
  3160. (('GetChangeRobotComments', 'x-review.googlesource.com',
  3161. 'infra%2Finfra~1'), {
  3162. 'codereview.settings': [
  3163. {
  3164. u'author': {
  3165. u'email': u'tricium@serviceaccount.com'
  3166. },
  3167. u'updated': u'2017-03-17 05:30:37.000000000',
  3168. u'robot_run_id': u'5565031076855808',
  3169. u'robot_id': u'Linter/Category',
  3170. u'tag': u'autogenerated:tricium',
  3171. u'patch_set': 2,
  3172. u'side': u'REVISION',
  3173. u'message': u'Linter warning message text',
  3174. u'line': 32,
  3175. },
  3176. ],
  3177. }),
  3178. ]
  3179. expected_comments_summary = [
  3180. git_cl._CommentSummary(
  3181. date=datetime.datetime(2017, 3, 17, 5, 30, 37),
  3182. message=(
  3183. u'(1 comment)\n\ncodereview.settings\n'
  3184. u' PS2, Line 32: https://x-review.googlesource.com/c/1/2/'
  3185. u'codereview.settings#32\n'
  3186. u' Linter warning message text\n'),
  3187. sender=u'tricium@serviceaccount.com',
  3188. autogenerated=True,
  3189. approval=False,
  3190. disapproval=False)
  3191. ]
  3192. cl = git_cl.Changelist(issue=1, branchref='refs/heads/foo')
  3193. self.assertEqual(cl.GetCommentsSummary(), expected_comments_summary)
  3194. def test_get_remote_url_with_mirror(self):
  3195. original_os_path_isdir = os.path.isdir
  3196. def selective_os_path_isdir_mock(path):
  3197. if path == '/cache/this-dir-exists':
  3198. return self._mocked_call('os.path.isdir', path)
  3199. return original_os_path_isdir(path)
  3200. mock.patch('os.path.isdir', selective_os_path_isdir_mock).start()
  3201. url = 'https://chromium.googlesource.com/my/repo'
  3202. scm.GIT.SetConfig('', 'remote.origin.url', '/cache/this-dir-exists')
  3203. scm.GIT.SetConfig('/cache/this-dir-exists', 'remote.origin.url', url)
  3204. self.calls = [
  3205. (('os.path.isdir', '/cache/this-dir-exists'), True),
  3206. ]
  3207. cl = git_cl.Changelist(issue=1)
  3208. self.assertEqual(cl.GetRemoteUrl(), url)
  3209. self.assertEqual(cl.GetRemoteUrl(), url) # Must be cached.
  3210. def test_get_remote_url_non_existing_mirror(self):
  3211. original_os_path_isdir = os.path.isdir
  3212. def selective_os_path_isdir_mock(path):
  3213. if path == '/cache/this-dir-doesnt-exist':
  3214. return self._mocked_call('os.path.isdir', path)
  3215. return original_os_path_isdir(path)
  3216. mock.patch('os.path.isdir', selective_os_path_isdir_mock).start()
  3217. mock.patch('logging.error',
  3218. lambda *a: self._mocked_call('logging.error', *a)).start()
  3219. scm.GIT.SetConfig('', 'remote.origin.url',
  3220. '/cache/this-dir-doesnt-exist')
  3221. self.calls = [
  3222. (('os.path.isdir', '/cache/this-dir-doesnt-exist'), False),
  3223. (('logging.error',
  3224. 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
  3225. 'but it doesn\'t exist.', {
  3226. 'remote': 'origin',
  3227. 'branch': 'main',
  3228. 'url': '/cache/this-dir-doesnt-exist'
  3229. }), None),
  3230. ]
  3231. cl = git_cl.Changelist(issue=1)
  3232. self.assertIsNone(cl.GetRemoteUrl())
  3233. def test_get_remote_url_misconfigured_mirror(self):
  3234. original_os_path_isdir = os.path.isdir
  3235. def selective_os_path_isdir_mock(path):
  3236. if path == '/cache/this-dir-exists':
  3237. return self._mocked_call('os.path.isdir', path)
  3238. return original_os_path_isdir(path)
  3239. mock.patch('os.path.isdir', selective_os_path_isdir_mock).start()
  3240. mock.patch('logging.error',
  3241. lambda *a: self._mocked_call('logging.error', *a)).start()
  3242. scm.GIT.SetConfig('', 'remote.origin.url', '/cache/this-dir-exists')
  3243. self.calls = [
  3244. (('os.path.isdir', '/cache/this-dir-exists'), True),
  3245. (('logging.error',
  3246. 'Remote "%(remote)s" for branch "%(branch)s" points to '
  3247. '"%(cache_path)s", but it is misconfigured.\n'
  3248. '"%(cache_path)s" must be a git repo and must have a remote named '
  3249. '"%(remote)s" pointing to the git host.', {
  3250. 'remote': 'origin',
  3251. 'cache_path': '/cache/this-dir-exists',
  3252. 'branch': 'main'
  3253. }), None),
  3254. ]
  3255. cl = git_cl.Changelist(issue=1)
  3256. self.assertIsNone(cl.GetRemoteUrl())
  3257. def test_gerrit_change_identifier_with_project(self):
  3258. scm.GIT.SetConfig('', 'remote.origin.url',
  3259. 'https://chromium.googlesource.com/a/my/repo.git/')
  3260. cl = git_cl.Changelist(issue=123456)
  3261. self.assertEqual(cl._GerritChangeIdentifier(), 'my%2Frepo~123456')
  3262. def test_gerrit_change_identifier_without_project(self):
  3263. mock.patch('logging.error',
  3264. lambda *a: self._mocked_call('logging.error', *a)).start()
  3265. self.calls = [
  3266. (('logging.error',
  3267. 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
  3268. 'but it doesn\'t exist.', {
  3269. 'remote': 'origin',
  3270. 'branch': 'main',
  3271. 'url': ''
  3272. }), None),
  3273. ]
  3274. cl = git_cl.Changelist(issue=123456)
  3275. self.assertEqual(cl._GerritChangeIdentifier(), '123456')
  3276. @unittest.skipIf(gclient_utils.IsEnvCog(),
  3277. 'not supported in non-git environment')
  3278. def test_gerrit_new_default(self):
  3279. self._run_gerrit_upload_test(
  3280. [],
  3281. 'desc ✔\n\nBUG=\n\nChange-Id: I123456789\n', [],
  3282. squash=False,
  3283. squash_mode='override_nosquash',
  3284. change_id='I123456789',
  3285. default_branch='main')
  3286. @unittest.skipIf(gclient_utils.IsEnvCog(),
  3287. 'not supported in non-git environment')
  3288. def test_gerrit_nosquash_with_issue(self):
  3289. self._run_gerrit_upload_test(
  3290. [],
  3291. 'desc ✔\n\nBUG=\n\nChange-Id: I123456789\n', [],
  3292. squash=False,
  3293. squash_mode='override_nosquash',
  3294. issue=123456,
  3295. change_id='I123456789',
  3296. default_branch='main')
  3297. class ChangelistTest(unittest.TestCase):
  3298. LAST_COMMIT_SUBJECT = 'Fixes goat teleporter destination to be Australia'
  3299. def _mock_run_git(commands):
  3300. if commands == ['show', '-s', '--format=%s', 'HEAD', '--']:
  3301. return ChangelistTest.LAST_COMMIT_SUBJECT
  3302. def setUp(self):
  3303. super(ChangelistTest, self).setUp()
  3304. mock.patch('gclient_utils.FileRead').start()
  3305. mock.patch('gclient_utils.FileWrite').start()
  3306. mock.patch('gclient_utils.temporary_file', TemporaryFileMock()).start()
  3307. mock.patch(
  3308. 'git_cl.Changelist.GetCodereviewServer',
  3309. return_value='https://chromium-review.googlesource.com').start()
  3310. mock.patch('git_cl.Changelist.GetAuthor', return_value='author').start()
  3311. mock.patch('git_cl.Changelist.GetIssue', return_value=123456).start()
  3312. mock.patch('git_cl.Changelist.GetPatchset', return_value=7).start()
  3313. mock.patch('git_cl.Changelist.GetRemoteBranch',
  3314. return_value=('origin', 'refs/remotes/origin/main')).start()
  3315. mock.patch('git_cl.PRESUBMIT_SUPPORT', 'PRESUBMIT_SUPPORT').start()
  3316. mock.patch('git_cl.Settings.GetRoot', return_value='root').start()
  3317. mock.patch('git_cl.Settings.GetIsGerrit', return_value=True).start()
  3318. mock.patch('git_cl.time_time').start()
  3319. mock.patch('metrics.collector').start()
  3320. mock.patch('subprocess2.Popen').start()
  3321. mock.patch('git_cl.Changelist.GetGerritProject',
  3322. return_value='project').start()
  3323. mock.patch('sys.exit', side_effect=SystemExitMock).start()
  3324. scm_mock.GIT(self)
  3325. self.addCleanup(mock.patch.stopall)
  3326. self.temp_count = 0
  3327. gerrit_util._Authenticator._resolved = None
  3328. def testRunHook(self):
  3329. expected_results = {
  3330. 'more_cc': ['cc@example.com', 'more@example.com'],
  3331. 'errors': [],
  3332. 'notifications': [],
  3333. 'warnings': [],
  3334. }
  3335. gclient_utils.FileRead.return_value = json.dumps(expected_results)
  3336. git_cl.time_time.side_effect = [100, 200, 300, 400]
  3337. mockProcess = mock.Mock()
  3338. mockProcess.wait.return_value = 0
  3339. subprocess2.Popen.return_value = mockProcess
  3340. cl = git_cl.Changelist()
  3341. results = cl.RunHook(committing=True,
  3342. may_prompt=True,
  3343. verbose=2,
  3344. parallel=True,
  3345. upstream='upstream',
  3346. description='description',
  3347. all_files=True,
  3348. resultdb=False)
  3349. self.assertEqual(expected_results, results)
  3350. subprocess2.Popen.assert_any_call([
  3351. 'vpython3',
  3352. 'PRESUBMIT_SUPPORT',
  3353. '--root',
  3354. 'root',
  3355. '--upstream',
  3356. 'upstream',
  3357. '--verbose',
  3358. '--verbose',
  3359. '--gerrit_url',
  3360. 'https://chromium-review.googlesource.com',
  3361. '--gerrit_project',
  3362. 'project',
  3363. '--gerrit_branch',
  3364. 'refs/heads/main',
  3365. '--author',
  3366. 'author',
  3367. '--issue',
  3368. '123456',
  3369. '--patchset',
  3370. '7',
  3371. '--commit',
  3372. '--may_prompt',
  3373. '--parallel',
  3374. '--all_files',
  3375. '--no_diffs',
  3376. '--json_output',
  3377. '/tmp/fake-temp2',
  3378. '--description_file',
  3379. '/tmp/fake-temp1',
  3380. ])
  3381. gclient_utils.FileWrite.assert_any_call('/tmp/fake-temp1',
  3382. 'description')
  3383. metrics.collector.add_repeated('sub_commands', {
  3384. 'command': 'presubmit',
  3385. 'execution_time': 100,
  3386. 'exit_code': 0,
  3387. })
  3388. def testRunHook_FewerOptions(self):
  3389. expected_results = {
  3390. 'more_cc': ['cc@example.com', 'more@example.com'],
  3391. 'errors': [],
  3392. 'notifications': [],
  3393. 'warnings': [],
  3394. }
  3395. gclient_utils.FileRead.return_value = json.dumps(expected_results)
  3396. git_cl.time_time.side_effect = [100, 200, 300, 400]
  3397. mockProcess = mock.Mock()
  3398. mockProcess.wait.return_value = 0
  3399. subprocess2.Popen.return_value = mockProcess
  3400. git_cl.Changelist.GetAuthor.return_value = None
  3401. git_cl.Changelist.GetIssue.return_value = None
  3402. git_cl.Changelist.GetPatchset.return_value = None
  3403. cl = git_cl.Changelist()
  3404. results = cl.RunHook(committing=False,
  3405. may_prompt=False,
  3406. verbose=0,
  3407. parallel=False,
  3408. upstream='upstream',
  3409. description='description',
  3410. all_files=False,
  3411. resultdb=False)
  3412. self.assertEqual(expected_results, results)
  3413. subprocess2.Popen.assert_any_call([
  3414. 'vpython3',
  3415. 'PRESUBMIT_SUPPORT',
  3416. '--root',
  3417. 'root',
  3418. '--upstream',
  3419. 'upstream',
  3420. '--gerrit_url',
  3421. 'https://chromium-review.googlesource.com',
  3422. '--gerrit_project',
  3423. 'project',
  3424. '--gerrit_branch',
  3425. 'refs/heads/main',
  3426. '--upload',
  3427. '--json_output',
  3428. '/tmp/fake-temp2',
  3429. '--description_file',
  3430. '/tmp/fake-temp1',
  3431. ])
  3432. gclient_utils.FileWrite.assert_any_call('/tmp/fake-temp1',
  3433. 'description')
  3434. metrics.collector.add_repeated('sub_commands', {
  3435. 'command': 'presubmit',
  3436. 'execution_time': 100,
  3437. 'exit_code': 0,
  3438. })
  3439. def testRunHook_FewerOptionsResultDB(self):
  3440. expected_results = {
  3441. 'more_cc': ['cc@example.com', 'more@example.com'],
  3442. 'errors': [],
  3443. 'notifications': [],
  3444. 'warnings': [],
  3445. }
  3446. gclient_utils.FileRead.return_value = json.dumps(expected_results)
  3447. git_cl.time_time.side_effect = [100, 200, 300, 400]
  3448. mockProcess = mock.Mock()
  3449. mockProcess.wait.return_value = 0
  3450. subprocess2.Popen.return_value = mockProcess
  3451. git_cl.Changelist.GetAuthor.return_value = None
  3452. git_cl.Changelist.GetIssue.return_value = None
  3453. git_cl.Changelist.GetPatchset.return_value = None
  3454. cl = git_cl.Changelist()
  3455. results = cl.RunHook(committing=False,
  3456. may_prompt=False,
  3457. verbose=0,
  3458. parallel=False,
  3459. upstream='upstream',
  3460. description='description',
  3461. all_files=False,
  3462. resultdb=True,
  3463. realm='chromium:public')
  3464. self.assertEqual(expected_results, results)
  3465. subprocess2.Popen.assert_any_call([
  3466. 'rdb',
  3467. 'stream',
  3468. '-new',
  3469. '-realm',
  3470. 'chromium:public',
  3471. '--',
  3472. 'vpython3',
  3473. 'PRESUBMIT_SUPPORT',
  3474. '--root',
  3475. 'root',
  3476. '--upstream',
  3477. 'upstream',
  3478. '--gerrit_url',
  3479. 'https://chromium-review.googlesource.com',
  3480. '--gerrit_project',
  3481. 'project',
  3482. '--gerrit_branch',
  3483. 'refs/heads/main',
  3484. '--upload',
  3485. '--json_output',
  3486. '/tmp/fake-temp2',
  3487. '--description_file',
  3488. '/tmp/fake-temp1',
  3489. ])
  3490. def testRunHook_NoGerrit(self):
  3491. mock.patch('git_cl.Settings.GetIsGerrit', return_value=False).start()
  3492. expected_results = {
  3493. 'more_cc': ['cc@example.com', 'more@example.com'],
  3494. 'errors': [],
  3495. 'notifications': [],
  3496. 'warnings': [],
  3497. }
  3498. gclient_utils.FileRead.return_value = json.dumps(expected_results)
  3499. git_cl.time_time.side_effect = [100, 200, 300, 400]
  3500. mockProcess = mock.Mock()
  3501. mockProcess.wait.return_value = 0
  3502. subprocess2.Popen.return_value = mockProcess
  3503. git_cl.Changelist.GetAuthor.return_value = None
  3504. git_cl.Changelist.GetIssue.return_value = None
  3505. git_cl.Changelist.GetPatchset.return_value = None
  3506. cl = git_cl.Changelist()
  3507. results = cl.RunHook(committing=False,
  3508. may_prompt=False,
  3509. verbose=0,
  3510. parallel=False,
  3511. upstream='upstream',
  3512. description='description',
  3513. all_files=False,
  3514. resultdb=False)
  3515. self.assertEqual(expected_results, results)
  3516. subprocess2.Popen.assert_any_call([
  3517. 'vpython3',
  3518. 'PRESUBMIT_SUPPORT',
  3519. '--root',
  3520. 'root',
  3521. '--upstream',
  3522. 'upstream',
  3523. '--upload',
  3524. '--json_output',
  3525. '/tmp/fake-temp2',
  3526. '--description_file',
  3527. '/tmp/fake-temp1',
  3528. ])
  3529. gclient_utils.FileWrite.assert_any_call('/tmp/fake-temp1',
  3530. 'description')
  3531. metrics.collector.add_repeated('sub_commands', {
  3532. 'command': 'presubmit',
  3533. 'execution_time': 100,
  3534. 'exit_code': 0,
  3535. })
  3536. @mock.patch('sys.exit', side_effect=SystemExitMock)
  3537. def testRunHook_Failure(self, _mock):
  3538. git_cl.time_time.side_effect = [100, 200]
  3539. mockProcess = mock.Mock()
  3540. mockProcess.wait.return_value = 2
  3541. subprocess2.Popen.return_value = mockProcess
  3542. cl = git_cl.Changelist()
  3543. with self.assertRaises(SystemExitMock):
  3544. cl.RunHook(committing=True,
  3545. may_prompt=True,
  3546. verbose=2,
  3547. parallel=True,
  3548. upstream='upstream',
  3549. description='description',
  3550. all_files=True,
  3551. resultdb=False)
  3552. sys.exit.assert_called_once_with(2)
  3553. def testRunPostUploadHook(self):
  3554. cl = git_cl.Changelist()
  3555. cl.RunPostUploadHook(2, 'upstream', 'description')
  3556. subprocess2.Popen.assert_called_with([
  3557. 'vpython3',
  3558. 'PRESUBMIT_SUPPORT',
  3559. '--root',
  3560. 'root',
  3561. '--upstream',
  3562. 'upstream',
  3563. '--verbose',
  3564. '--verbose',
  3565. '--gerrit_url',
  3566. 'https://chromium-review.googlesource.com',
  3567. '--gerrit_project',
  3568. 'project',
  3569. '--gerrit_branch',
  3570. 'refs/heads/main',
  3571. '--author',
  3572. 'author',
  3573. '--issue',
  3574. '123456',
  3575. '--patchset',
  3576. '7',
  3577. '--post_upload',
  3578. '--description_file',
  3579. '/tmp/fake-temp1',
  3580. ])
  3581. gclient_utils.FileWrite.assert_called_once_with('/tmp/fake-temp1',
  3582. 'description')
  3583. def testRunPostUploadHookPy3Only(self):
  3584. cl = git_cl.Changelist()
  3585. cl.RunPostUploadHook(2, 'upstream', 'description')
  3586. subprocess2.Popen.assert_called_once_with([
  3587. 'vpython3',
  3588. 'PRESUBMIT_SUPPORT',
  3589. '--root',
  3590. 'root',
  3591. '--upstream',
  3592. 'upstream',
  3593. '--verbose',
  3594. '--verbose',
  3595. '--gerrit_url',
  3596. 'https://chromium-review.googlesource.com',
  3597. '--gerrit_project',
  3598. 'project',
  3599. '--gerrit_branch',
  3600. 'refs/heads/main',
  3601. '--author',
  3602. 'author',
  3603. '--issue',
  3604. '123456',
  3605. '--patchset',
  3606. '7',
  3607. '--post_upload',
  3608. '--description_file',
  3609. '/tmp/fake-temp1',
  3610. ])
  3611. gclient_utils.FileWrite.assert_called_once_with('/tmp/fake-temp1',
  3612. 'description')
  3613. @mock.patch('git_cl.RunGit', _mock_run_git)
  3614. def testDefaultTitleEmptyMessage(self):
  3615. cl = git_cl.Changelist()
  3616. cl.issue = 100
  3617. options = optparse.Values({
  3618. 'squash': True,
  3619. 'title': None,
  3620. 'message': None,
  3621. 'force': None,
  3622. 'skip_title': None
  3623. })
  3624. mock.patch('gclient_utils.AskForData', lambda _: user_title).start()
  3625. for user_title in ['', 'y', 'Y']:
  3626. self.assertEqual(cl._GetTitleForUpload(options),
  3627. self.LAST_COMMIT_SUBJECT)
  3628. for user_title in ['not empty', 'yes', 'YES']:
  3629. self.assertEqual(cl._GetTitleForUpload(options), user_title)
  3630. @mock.patch('git_cl.Changelist.GetMostRecentPatchset', return_value=2)
  3631. @mock.patch('git_cl.RunGit')
  3632. @mock.patch('git_cl.Changelist._PrepareChange')
  3633. def testPrepareSquashedCommit(self, mockPrepareChange, mockRunGit, *_mocks):
  3634. change_desc = git_cl.ChangeDescription('BOO!')
  3635. reviewers = []
  3636. ccs = []
  3637. mockPrepareChange.return_value = (reviewers, ccs, change_desc)
  3638. parent_hash = 'upstream-gerrit-hash'
  3639. parent_orig_hash = 'upstream-last-upload-hash'
  3640. parent_hash_root = 'root-commit'
  3641. hash_to_push = 'new-squash-hash'
  3642. hash_to_push_root = 'new-squash-hash-root'
  3643. branchref = 'refs/heads/current-branch'
  3644. end_hash = 'end-hash'
  3645. tree_hash = 'tree-hash'
  3646. def mock_run_git(commands):
  3647. if {'commit-tree', tree_hash, '-p', parent_hash,
  3648. '-F'}.issubset(set(commands)):
  3649. return hash_to_push
  3650. if {'commit-tree', tree_hash, '-p', parent_hash_root,
  3651. '-F'}.issubset(set(commands)):
  3652. return hash_to_push_root
  3653. if commands == ['rev-parse', branchref]:
  3654. return end_hash
  3655. if commands == ['rev-parse', end_hash + ':']:
  3656. return tree_hash
  3657. mockRunGit.side_effect = mock_run_git
  3658. cl = git_cl.Changelist(branchref=branchref)
  3659. options = optparse.Values()
  3660. new_upload = cl.PrepareSquashedCommit(options, parent_hash,
  3661. parent_orig_hash)
  3662. self.assertEqual(new_upload.reviewers, reviewers)
  3663. self.assertEqual(new_upload.ccs, ccs)
  3664. self.assertEqual(new_upload.commit_to_push, hash_to_push)
  3665. self.assertEqual(new_upload.new_last_uploaded_commit, end_hash)
  3666. self.assertEqual(new_upload.change_desc, change_desc)
  3667. mockPrepareChange.assert_called_with(options, parent_orig_hash,
  3668. end_hash)
  3669. @mock.patch('git_cl.Settings.GetRoot', return_value='')
  3670. @mock.patch('git_cl.Changelist.GetMostRecentPatchset', return_value=2)
  3671. @mock.patch('git_cl.RunGitWithCode')
  3672. @mock.patch('git_cl.RunGit')
  3673. @mock.patch('git_cl.Changelist._PrepareChange')
  3674. @mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
  3675. def testPrepareCherryPickSquashedCommit(self,
  3676. mockGetCommonAncestorWithUpstream,
  3677. mockPrepareChange, mockRunGit,
  3678. mockRunGitWithCode, *_mocks):
  3679. cherry_pick_base_hash = '1a2bcherrypickbase'
  3680. mockGetCommonAncestorWithUpstream.return_value = cherry_pick_base_hash
  3681. change_desc = git_cl.ChangeDescription('BOO!')
  3682. ccs = ['cc@review.cl']
  3683. reviewers = ['reviewer@review.cl']
  3684. mockPrepareChange.return_value = (reviewers, ccs, change_desc)
  3685. branchref = 'refs/heads/current-branch'
  3686. cl = git_cl.Changelist(branchref=branchref)
  3687. options = optparse.Values()
  3688. upstream_gerrit_hash = 'upstream-gerrit-hash'
  3689. latest_tree_hash = 'tree-hash'
  3690. hash_to_cp = 'squashed-hash'
  3691. hash_to_push = 'hash-to-push'
  3692. hash_to_save_as_last_upload = 'last-upload'
  3693. def mock_run_git(commands):
  3694. if commands == ['rev-parse', branchref]:
  3695. return hash_to_save_as_last_upload
  3696. if commands == ['rev-parse', branchref + ':']:
  3697. return latest_tree_hash
  3698. if {
  3699. 'commit-tree', latest_tree_hash, '-p',
  3700. cherry_pick_base_hash, '-F'
  3701. }.issubset(set(commands)):
  3702. return hash_to_cp
  3703. if commands == ['rev-parse', 'HEAD']:
  3704. return hash_to_push
  3705. mockRunGit.side_effect = mock_run_git
  3706. def mock_run_git_with_code(commands):
  3707. if commands == ['cherry-pick', hash_to_cp]:
  3708. return 0, ''
  3709. mockRunGitWithCode.side_effect = mock_run_git_with_code
  3710. new_upload = cl.PrepareCherryPickSquashedCommit(options,
  3711. upstream_gerrit_hash)
  3712. self.assertEqual(new_upload.reviewers, reviewers)
  3713. self.assertEqual(new_upload.ccs, ccs)
  3714. self.assertEqual(new_upload.commit_to_push, hash_to_push)
  3715. self.assertEqual(new_upload.new_last_uploaded_commit,
  3716. hash_to_save_as_last_upload)
  3717. self.assertEqual(new_upload.change_desc, change_desc)
  3718. # Test failed cherry-pick
  3719. def mock_run_git_with_code(commands):
  3720. if commands == ['cherry-pick', hash_to_cp]:
  3721. return 1, ''
  3722. mockRunGitWithCode.side_effect = mock_run_git_with_code
  3723. with self.assertRaises(SystemExitMock):
  3724. cl.PrepareCherryPickSquashedCommit(options, cherry_pick_base_hash)
  3725. @mock.patch('git_cl.Settings.GetDefaultCCList', return_value=[])
  3726. @mock.patch('git_cl.Changelist.GetAffectedFiles', return_value=[])
  3727. @mock.patch('git_cl.GenerateGerritChangeId', return_value='1a2b3c')
  3728. @mock.patch('git_cl.Changelist.GetIssue', return_value=None)
  3729. @mock.patch('git_cl.ChangeDescription.prompt')
  3730. @mock.patch('git_cl.Changelist.RunHook')
  3731. @mock.patch('git_cl.Changelist._GetDescriptionForUpload')
  3732. @mock.patch('git_cl.Changelist.EnsureCanUploadPatchset')
  3733. def testPrepareChange_new(self, mockEnsureCanUploadPatchset,
  3734. mockGetDescriptionForupload, mockRunHook,
  3735. mockPrompt, *_mocks):
  3736. options = optparse.Values()
  3737. options.force = False
  3738. options.bypass_hooks = False
  3739. options.verbose = False
  3740. options.parallel = False
  3741. options.preserve_tryjobs = False
  3742. options.private = False
  3743. options.no_autocc = False
  3744. options.message_file = None
  3745. options.commit_description = None
  3746. options.cc = ['chicken@bok.farm']
  3747. parent = '420parent'
  3748. latest_tree = '420latest_tree'
  3749. mockRunHook.return_value = {'more_cc': ['cow@moo.farm']}
  3750. desc = 'AH!\nCC=cow2@moo.farm\nR=horse@apple.farm'
  3751. mockGetDescriptionForupload.return_value = git_cl.ChangeDescription(
  3752. desc)
  3753. cl = git_cl.Changelist()
  3754. reviewers, ccs, change_desc = cl._PrepareChange(options, parent,
  3755. latest_tree)
  3756. self.assertEqual(reviewers, ['horse@apple.farm'])
  3757. self.assertEqual(ccs,
  3758. ['cow@moo.farm', 'chicken@bok.farm', 'cow2@moo.farm'])
  3759. self.assertEqual(change_desc._description_lines, [
  3760. 'AH!', 'CC=cow2@moo.farm', 'R=horse@apple.farm', '',
  3761. 'Change-Id: 1a2b3c'
  3762. ])
  3763. mockPrompt.assert_called_once()
  3764. mockEnsureCanUploadPatchset.assert_called_once()
  3765. mockRunHook.assert_called_once_with(committing=False,
  3766. may_prompt=True,
  3767. verbose=False,
  3768. parallel=False,
  3769. upstream='420parent',
  3770. description=desc,
  3771. all_files=False,
  3772. end_commit='420latest_tree')
  3773. @mock.patch('git_cl.Changelist.GetAffectedFiles', return_value=[])
  3774. @mock.patch('git_cl.Changelist.GetIssue', return_value='123')
  3775. @mock.patch('git_cl.ChangeDescription.prompt')
  3776. @mock.patch('gerrit_util.GetChangeDetail')
  3777. @mock.patch('git_cl.Changelist.RunHook')
  3778. @mock.patch('git_cl.Changelist._GetDescriptionForUpload')
  3779. @mock.patch('git_cl.Changelist.EnsureCanUploadPatchset')
  3780. def testPrepareChange_existing(self, mockEnsureCanUploadPatchset,
  3781. mockGetDescriptionForupload, mockRunHook,
  3782. mockGetChangeDetail, mockPrompt, *_mocks):
  3783. cl = git_cl.Changelist()
  3784. options = optparse.Values()
  3785. options.force = False
  3786. options.bypass_hooks = False
  3787. options.verbose = False
  3788. options.parallel = False
  3789. options.edit_description = False
  3790. options.preserve_tryjobs = False
  3791. options.private = False
  3792. options.no_autocc = False
  3793. options.cc = ['chicken@bok.farm']
  3794. parent = '420parent'
  3795. latest_tree = '420latest_tree'
  3796. mockRunHook.return_value = {'more_cc': ['cow@moo.farm']}
  3797. desc = 'AH!\nCC=cow2@moo.farm\nR=horse@apple.farm'
  3798. mockGetDescriptionForupload.return_value = git_cl.ChangeDescription(
  3799. desc)
  3800. # Existing change
  3801. gerrit_util.GetChangeDetail.return_value = {
  3802. 'change_id': ('123456789'),
  3803. 'current_revision': 'sha1_of_current_revision',
  3804. }
  3805. reviewers, ccs, change_desc = cl._PrepareChange(options, parent,
  3806. latest_tree)
  3807. self.assertEqual(reviewers, ['horse@apple.farm'])
  3808. self.assertEqual(ccs, ['chicken@bok.farm', 'cow2@moo.farm'])
  3809. self.assertEqual(change_desc._description_lines, [
  3810. 'AH!', 'CC=cow2@moo.farm', 'R=horse@apple.farm', '',
  3811. 'Change-Id: 123456789'
  3812. ])
  3813. mockRunHook.assert_called_once_with(committing=False,
  3814. may_prompt=True,
  3815. verbose=False,
  3816. parallel=False,
  3817. upstream=parent,
  3818. description=desc,
  3819. all_files=False,
  3820. end_commit=latest_tree)
  3821. mockEnsureCanUploadPatchset.assert_called_once()
  3822. # Test preserve_tryjob
  3823. options.preserve_tryjobs = True
  3824. # Test edit_description
  3825. options.edit_description = True
  3826. # Test private
  3827. options.private = True
  3828. options.no_autocc = True
  3829. reviewers, ccs, change_desc = cl._PrepareChange(options, parent,
  3830. latest_tree)
  3831. self.assertEqual(ccs, ['chicken@bok.farm', 'cow2@moo.farm'])
  3832. mockPrompt.assert_called_once()
  3833. self.assertEqual(change_desc._description_lines, [
  3834. 'AH!', 'CC=cow2@moo.farm', 'R=horse@apple.farm', '',
  3835. 'Change-Id: 123456789', 'Cq-Do-Not-Cancel-Tryjobs: true'
  3836. ])
  3837. @mock.patch('git_cl.Changelist.GetGerritHost', return_value='chromium')
  3838. @mock.patch('git_cl.Settings.GetRunPostUploadHook', return_value=True)
  3839. @mock.patch('git_cl.Changelist.SetPatchset')
  3840. @mock.patch('git_cl.Changelist.RunPostUploadHook')
  3841. @mock.patch('git_cl.gerrit_util.AddReviewers')
  3842. def testPostUploadUpdates(self, mockAddReviewers, mockRunPostHook,
  3843. mockSetPatchset, *_mocks):
  3844. cl = git_cl.Changelist(branchref='refs/heads/current-branch')
  3845. options = optparse.Values()
  3846. options.verbose = True
  3847. options.no_python2_post_upload_hooks = True
  3848. options.send_mail = False
  3849. reviewers = ['monkey@vp.circus']
  3850. ccs = ['cow@rds.corp']
  3851. change_desc = git_cl.ChangeDescription('[stonks] honk honk')
  3852. new_upload = git_cl._NewUpload(reviewers, ccs, 'pushed-commit',
  3853. 'last-uploaded-commit', 'parent-commit',
  3854. change_desc, 2)
  3855. cl.PostUploadUpdates(options, new_upload, '12345')
  3856. mockSetPatchset.assert_called_once_with(3)
  3857. self.assertEqual(
  3858. scm.GIT.GetConfig('root', 'branch.current-branch.gerritsquashhash'),
  3859. new_upload.commit_to_push)
  3860. self.assertEqual(
  3861. scm.GIT.GetConfig('root', 'branch.current-branch.last-upload-hash'),
  3862. new_upload.new_last_uploaded_commit)
  3863. mockAddReviewers.assert_called_once_with('chromium',
  3864. 'project~123456',
  3865. reviewers=reviewers,
  3866. ccs=ccs,
  3867. notify=False)
  3868. mockRunPostHook.assert_called_once_with(True, 'parent-commit',
  3869. change_desc.description)
  3870. class CMDTestCaseBase(unittest.TestCase):
  3871. _STATUSES = [
  3872. 'STATUS_UNSPECIFIED',
  3873. 'SCHEDULED',
  3874. 'STARTED',
  3875. 'SUCCESS',
  3876. 'FAILURE',
  3877. 'INFRA_FAILURE',
  3878. 'CANCELED',
  3879. ]
  3880. _CHANGE_DETAIL = {
  3881. 'project': 'depot_tools',
  3882. 'status': 'OPEN',
  3883. 'owner': {
  3884. 'email': 'owner@e.mail'
  3885. },
  3886. 'current_revision': 'beeeeeef',
  3887. 'revisions': {
  3888. 'deadbeaf': {
  3889. '_number': 6,
  3890. 'kind': 'REWORK',
  3891. },
  3892. 'beeeeeef': {
  3893. '_number': 7,
  3894. 'kind': 'NO_CODE_CHANGE',
  3895. 'fetch': {
  3896. 'http': {
  3897. 'url': 'https://chromium.googlesource.com/depot_tools',
  3898. 'ref': 'refs/changes/56/123456/7'
  3899. }
  3900. },
  3901. },
  3902. },
  3903. }
  3904. _DEFAULT_RESPONSE = {
  3905. 'builds': [{
  3906. 'id': str(100 + idx),
  3907. 'builder': {
  3908. 'project': 'chromium',
  3909. 'bucket': 'try',
  3910. 'builder': 'bot_' + status.lower(),
  3911. },
  3912. 'createTime': '2019-10-09T08:00:0%d.854286Z' % (idx % 10),
  3913. 'tags': [],
  3914. 'status': status,
  3915. } for idx, status in enumerate(_STATUSES)]
  3916. }
  3917. def setUp(self):
  3918. super(CMDTestCaseBase, self).setUp()
  3919. mock.patch('git_cl.sys.stdout', io.StringIO()).start()
  3920. mock.patch('git_cl.uuid.uuid4', return_value='uuid4').start()
  3921. mock.patch('git_cl.Changelist.GetIssue', return_value=123456).start()
  3922. mock.patch(
  3923. 'git_cl.Changelist.GetCodereviewServer',
  3924. return_value='https://chromium-review.googlesource.com').start()
  3925. mock.patch('git_cl.Changelist.GetGerritHost',
  3926. return_value='chromium-review.googlesource.com').start()
  3927. mock.patch('git_cl.Changelist.GetMostRecentPatchset',
  3928. return_value=7).start()
  3929. mock.patch('git_cl.Changelist.GetMostRecentDryRunPatchset',
  3930. return_value=6).start()
  3931. mock.patch('git_cl.Changelist.GetRemoteUrl',
  3932. return_value='https://chromium.googlesource.com/depot_tools'
  3933. ).start()
  3934. mock.patch('auth.Authenticator',
  3935. return_value=AuthenticatorMock()).start()
  3936. mock.patch('gerrit_util.GetChangeDetail',
  3937. return_value=self._CHANGE_DETAIL).start()
  3938. mock.patch('git_cl._call_buildbucket',
  3939. return_value=self._DEFAULT_RESPONSE).start()
  3940. mock.patch('git_common.is_dirty_git_tree', return_value=False).start()
  3941. self.addCleanup(mock.patch.stopall)
  3942. @unittest.skipIf(gclient_utils.IsEnvCog(),
  3943. 'not supported in non-git environment')
  3944. class CMDPresubmitTestCase(CMDTestCaseBase):
  3945. _RUN_HOOK_RETURN = {
  3946. 'errors': [],
  3947. 'more_cc': [],
  3948. 'notifications': [],
  3949. 'warnings': []
  3950. }
  3951. def setUp(self):
  3952. super(CMDPresubmitTestCase, self).setUp()
  3953. mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream',
  3954. return_value='upstream').start()
  3955. mock.patch('git_cl.Changelist.FetchDescription',
  3956. return_value='fetch description').start()
  3957. mock.patch('git_cl._create_description_from_log',
  3958. return_value='get description').start()
  3959. mock.patch('git_cl.Changelist.RunHook',
  3960. return_value=self._RUN_HOOK_RETURN).start()
  3961. def testDefaultCase(self):
  3962. self.assertEqual(0, git_cl.main(['presubmit']))
  3963. git_cl.Changelist.RunHook.assert_called_once_with(
  3964. committing=True,
  3965. may_prompt=False,
  3966. verbose=0,
  3967. parallel=None,
  3968. upstream='upstream',
  3969. description='fetch description',
  3970. all_files=None,
  3971. files=None,
  3972. resultdb=None,
  3973. realm=None)
  3974. def testNoIssue(self):
  3975. git_cl.Changelist.GetIssue.return_value = None
  3976. self.assertEqual(0, git_cl.main(['presubmit']))
  3977. git_cl.Changelist.RunHook.assert_called_once_with(
  3978. committing=True,
  3979. may_prompt=False,
  3980. verbose=0,
  3981. parallel=None,
  3982. upstream='upstream',
  3983. description='get description',
  3984. all_files=None,
  3985. files=None,
  3986. resultdb=None,
  3987. realm=None)
  3988. def testCustomBranch(self):
  3989. self.assertEqual(0, git_cl.main(['presubmit', 'custom_branch']))
  3990. git_cl.Changelist.RunHook.assert_called_once_with(
  3991. committing=True,
  3992. may_prompt=False,
  3993. verbose=0,
  3994. parallel=None,
  3995. upstream='custom_branch',
  3996. description='fetch description',
  3997. all_files=None,
  3998. files=None,
  3999. resultdb=None,
  4000. realm=None)
  4001. def testOptions(self):
  4002. self.assertEqual(
  4003. 0,
  4004. git_cl.main([
  4005. 'presubmit', '-v', '-v', '--all', '--parallel', '-u',
  4006. '--resultdb', '--realm', 'chromium:public'
  4007. ]))
  4008. git_cl.Changelist.RunHook.assert_called_once_with(
  4009. committing=False,
  4010. may_prompt=False,
  4011. verbose=2,
  4012. parallel=True,
  4013. upstream='upstream',
  4014. description='fetch description',
  4015. all_files=True,
  4016. files=None,
  4017. resultdb=True,
  4018. realm='chromium:public')
  4019. @mock.patch('git_cl.write_json')
  4020. def testJson(self, mock_write_json):
  4021. self.assertEqual(0, git_cl.main(['presubmit', '--json', 'file.json']))
  4022. mock_write_json.assert_called_once_with('file.json',
  4023. self._RUN_HOOK_RETURN)
  4024. class CMDTryResultsTestCase(CMDTestCaseBase):
  4025. _DEFAULT_REQUEST = {
  4026. 'predicate': {
  4027. "gerritChanges": [{
  4028. "project": "depot_tools",
  4029. "host": "chromium-review.googlesource.com",
  4030. "patchset": 6,
  4031. "change": 123456,
  4032. }],
  4033. },
  4034. 'fields': ('builds.*.id,builds.*.builder,builds.*.status' +
  4035. ',builds.*.createTime,builds.*.tags'),
  4036. }
  4037. _TRIVIAL_REQUEST = {
  4038. 'predicate': {
  4039. "gerritChanges": [{
  4040. "project": "depot_tools",
  4041. "host": "chromium-review.googlesource.com",
  4042. "patchset": 7,
  4043. "change": 123456,
  4044. }],
  4045. },
  4046. 'fields': ('builds.*.id,builds.*.builder,builds.*.status' +
  4047. ',builds.*.createTime,builds.*.tags'),
  4048. }
  4049. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4050. 'not supported in non-git environment')
  4051. def testNoJobs(self):
  4052. git_cl._call_buildbucket.return_value = {}
  4053. self.assertEqual(0, git_cl.main(['try-results']))
  4054. self.assertEqual('No tryjobs scheduled.\n', sys.stdout.getvalue())
  4055. git_cl._call_buildbucket.assert_called_once_with(
  4056. mock.ANY, 'cr-buildbucket.appspot.com', 'SearchBuilds',
  4057. self._DEFAULT_REQUEST)
  4058. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4059. 'not supported in non-git environment')
  4060. def testTrivialCommits(self):
  4061. self.assertEqual(0, git_cl.main(['try-results']))
  4062. git_cl._call_buildbucket.assert_called_with(
  4063. mock.ANY, 'cr-buildbucket.appspot.com', 'SearchBuilds',
  4064. self._DEFAULT_REQUEST)
  4065. git_cl._call_buildbucket.return_value = {}
  4066. self.assertEqual(0, git_cl.main(['try-results', '--patchset', '7']))
  4067. git_cl._call_buildbucket.assert_called_with(
  4068. mock.ANY, 'cr-buildbucket.appspot.com', 'SearchBuilds',
  4069. self._TRIVIAL_REQUEST)
  4070. self.assertEqual([
  4071. 'Successes:',
  4072. ' bot_success https://ci.chromium.org/b/103',
  4073. 'Infra Failures:',
  4074. ' bot_infra_failure https://ci.chromium.org/b/105',
  4075. 'Failures:',
  4076. ' bot_failure https://ci.chromium.org/b/104',
  4077. 'Canceled:',
  4078. ' bot_canceled ',
  4079. 'Started:',
  4080. ' bot_started https://ci.chromium.org/b/102',
  4081. 'Scheduled:',
  4082. ' bot_scheduled id=101',
  4083. 'Other:',
  4084. ' bot_status_unspecified id=100',
  4085. 'Total: 7 tryjobs',
  4086. 'No tryjobs scheduled.',
  4087. ],
  4088. sys.stdout.getvalue().splitlines())
  4089. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4090. 'not supported in non-git environment')
  4091. def testPrintToStdout(self):
  4092. self.assertEqual(0, git_cl.main(['try-results']))
  4093. self.assertEqual([
  4094. 'Successes:',
  4095. ' bot_success https://ci.chromium.org/b/103',
  4096. 'Infra Failures:',
  4097. ' bot_infra_failure https://ci.chromium.org/b/105',
  4098. 'Failures:',
  4099. ' bot_failure https://ci.chromium.org/b/104',
  4100. 'Canceled:',
  4101. ' bot_canceled ',
  4102. 'Started:',
  4103. ' bot_started https://ci.chromium.org/b/102',
  4104. 'Scheduled:',
  4105. ' bot_scheduled id=101',
  4106. 'Other:',
  4107. ' bot_status_unspecified id=100',
  4108. 'Total: 7 tryjobs',
  4109. ],
  4110. sys.stdout.getvalue().splitlines())
  4111. git_cl._call_buildbucket.assert_called_once_with(
  4112. mock.ANY, 'cr-buildbucket.appspot.com', 'SearchBuilds',
  4113. self._DEFAULT_REQUEST)
  4114. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4115. 'not supported in non-git environment')
  4116. def testPrintToStdoutWithMasters(self):
  4117. self.assertEqual(0, git_cl.main(['try-results', '--print-master']))
  4118. self.assertEqual([
  4119. 'Successes:',
  4120. ' try bot_success https://ci.chromium.org/b/103',
  4121. 'Infra Failures:',
  4122. ' try bot_infra_failure https://ci.chromium.org/b/105',
  4123. 'Failures:',
  4124. ' try bot_failure https://ci.chromium.org/b/104',
  4125. 'Canceled:',
  4126. ' try bot_canceled ',
  4127. 'Started:',
  4128. ' try bot_started https://ci.chromium.org/b/102',
  4129. 'Scheduled:',
  4130. ' try bot_scheduled id=101',
  4131. 'Other:',
  4132. ' try bot_status_unspecified id=100',
  4133. 'Total: 7 tryjobs',
  4134. ],
  4135. sys.stdout.getvalue().splitlines())
  4136. git_cl._call_buildbucket.assert_called_once_with(
  4137. mock.ANY, 'cr-buildbucket.appspot.com', 'SearchBuilds',
  4138. self._DEFAULT_REQUEST)
  4139. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4140. 'not supported in non-git environment')
  4141. @mock.patch('git_cl.write_json')
  4142. def testWriteToJson(self, mockJsonDump):
  4143. self.assertEqual(0, git_cl.main(['try-results', '--json', 'file.json']))
  4144. git_cl._call_buildbucket.assert_called_once_with(
  4145. mock.ANY, 'cr-buildbucket.appspot.com', 'SearchBuilds',
  4146. self._DEFAULT_REQUEST)
  4147. mockJsonDump.assert_called_once_with('file.json',
  4148. self._DEFAULT_RESPONSE['builds'])
  4149. def test_filter_failed_for_one_simple(self):
  4150. self.assertEqual([], git_cl._filter_failed_for_retry([]))
  4151. self.assertEqual([
  4152. ('chromium', 'try', 'bot_failure'),
  4153. ('chromium', 'try', 'bot_infra_failure'),
  4154. ], git_cl._filter_failed_for_retry(self._DEFAULT_RESPONSE['builds']))
  4155. def test_filter_failed_for_retry_many_builds(self):
  4156. def _build(name, created_sec, status, experimental=False):
  4157. assert 0 <= created_sec < 100, created_sec
  4158. b = {
  4159. 'id': 112112,
  4160. 'builder': {
  4161. 'project': 'chromium',
  4162. 'bucket': 'try',
  4163. 'builder': name,
  4164. },
  4165. 'createTime': '2019-10-09T08:00:%02d.854286Z' % created_sec,
  4166. 'status': status,
  4167. 'tags': [],
  4168. }
  4169. if experimental:
  4170. b['tags'].append({'key': 'cq_experimental', 'value': 'true'})
  4171. return b
  4172. builds = [
  4173. _build('flaky-last-green', 1, 'FAILURE'),
  4174. _build('flaky-last-green', 2, 'SUCCESS'),
  4175. _build('flaky', 1, 'SUCCESS'),
  4176. _build('flaky', 2, 'FAILURE'),
  4177. _build('running', 1, 'FAILED'),
  4178. _build('running', 2, 'SCHEDULED'),
  4179. _build('yep-still-running', 1, 'STARTED'),
  4180. _build('yep-still-running', 2, 'FAILURE'),
  4181. _build('cq-experimental', 1, 'SUCCESS', experimental=True),
  4182. _build('cq-experimental', 2, 'FAILURE', experimental=True),
  4183. # Simulate experimental in CQ builder, which developer decided
  4184. # to retry manually which resulted in 2nd build non-experimental.
  4185. _build('sometimes-experimental', 1, 'FAILURE', experimental=True),
  4186. _build('sometimes-experimental', 2, 'FAILURE', experimental=False),
  4187. ]
  4188. builds.sort(key=lambda b: b['status']) # ~deterministic shuffle.
  4189. self.assertEqual([
  4190. ('chromium', 'try', 'flaky'),
  4191. ('chromium', 'try', 'sometimes-experimental'),
  4192. ], git_cl._filter_failed_for_retry(builds))
  4193. class CMDTryTestCase(CMDTestCaseBase):
  4194. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4195. 'not supported in non-git environment')
  4196. @mock.patch('git_cl.Changelist.SetCQState')
  4197. def testSetCQDryRunByDefault(self, mockSetCQState):
  4198. mockSetCQState.return_value = 0
  4199. self.assertEqual(0, git_cl.main(['try']))
  4200. git_cl.Changelist.SetCQState.assert_called_with(git_cl._CQState.DRY_RUN)
  4201. self.assertEqual(
  4202. sys.stdout.getvalue(), 'Scheduling CQ dry run on: '
  4203. 'https://chromium-review.googlesource.com/123456\n')
  4204. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4205. 'not supported in non-git environment')
  4206. @mock.patch('git_cl._call_buildbucket')
  4207. def testScheduleOnBuildbucket(self, mockCallBuildbucket):
  4208. mockCallBuildbucket.return_value = {}
  4209. self.assertEqual(
  4210. 0,
  4211. git_cl.main([
  4212. 'try', '-B', 'luci.chromium.try', '-b', 'win', '-p', 'key=val',
  4213. '-p', 'json=[{"a":1}, null]'
  4214. ]))
  4215. self.assertIn('Scheduling jobs on:\n'
  4216. ' chromium/try: win', git_cl.sys.stdout.getvalue())
  4217. expected_request = {
  4218. "requests": [{
  4219. "scheduleBuild": {
  4220. "requestId":
  4221. "uuid4",
  4222. "builder": {
  4223. "project": "chromium",
  4224. "builder": "win",
  4225. "bucket": "try",
  4226. },
  4227. "gerritChanges": [{
  4228. "project": "depot_tools",
  4229. "host": "chromium-review.googlesource.com",
  4230. "patchset": 7,
  4231. "change": 123456,
  4232. }],
  4233. "properties": {
  4234. "category": "git_cl_try",
  4235. "json": [{
  4236. "a": 1
  4237. }, None],
  4238. "key": "val",
  4239. },
  4240. "tags": [
  4241. {
  4242. "value": "win",
  4243. "key": "builder"
  4244. },
  4245. {
  4246. "value": "git_cl_try",
  4247. "key": "user_agent"
  4248. },
  4249. ],
  4250. },
  4251. }],
  4252. }
  4253. mockCallBuildbucket.assert_called_with(mock.ANY,
  4254. 'cr-buildbucket.appspot.com',
  4255. 'Batch', expected_request)
  4256. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4257. 'not supported in non-git environment')
  4258. @mock.patch('git_cl._call_buildbucket')
  4259. def testScheduleOnBuildbucketWithRevision(self, mockCallBuildbucket):
  4260. mockCallBuildbucket.return_value = {}
  4261. mock.patch('git_cl.Changelist.GetRemoteBranch',
  4262. return_value=('origin', 'refs/remotes/origin/main')).start()
  4263. self.assertEqual(
  4264. 0,
  4265. git_cl.main([
  4266. 'try', '-B', 'luci.chromium.try', '-b', 'win', '-b', 'linux',
  4267. '-p', 'key=val', '-p', 'json=[{"a":1}, null]', '-r',
  4268. 'beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef'
  4269. ]))
  4270. self.assertIn(
  4271. 'Scheduling jobs on:\n'
  4272. ' chromium/try: linux\n'
  4273. ' chromium/try: win', git_cl.sys.stdout.getvalue())
  4274. expected_request = {
  4275. "requests": [{
  4276. "scheduleBuild": {
  4277. "requestId":
  4278. "uuid4",
  4279. "builder": {
  4280. "project": "chromium",
  4281. "builder": "linux",
  4282. "bucket": "try",
  4283. },
  4284. "gerritChanges": [{
  4285. "project": "depot_tools",
  4286. "host": "chromium-review.googlesource.com",
  4287. "patchset": 7,
  4288. "change": 123456,
  4289. }],
  4290. "properties": {
  4291. "category": "git_cl_try",
  4292. "json": [{
  4293. "a": 1
  4294. }, None],
  4295. "key": "val",
  4296. },
  4297. "tags": [
  4298. {
  4299. "value": "linux",
  4300. "key": "builder"
  4301. },
  4302. {
  4303. "value": "git_cl_try",
  4304. "key": "user_agent"
  4305. },
  4306. ],
  4307. "gitilesCommit": {
  4308. "host": "chromium.googlesource.com",
  4309. "project": "depot_tools",
  4310. "id": "beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef",
  4311. "ref": "refs/heads/main",
  4312. }
  4313. },
  4314. }, {
  4315. "scheduleBuild": {
  4316. "requestId":
  4317. "uuid4",
  4318. "builder": {
  4319. "project": "chromium",
  4320. "builder": "win",
  4321. "bucket": "try",
  4322. },
  4323. "gerritChanges": [{
  4324. "project": "depot_tools",
  4325. "host": "chromium-review.googlesource.com",
  4326. "patchset": 7,
  4327. "change": 123456,
  4328. }],
  4329. "properties": {
  4330. "category": "git_cl_try",
  4331. "json": [{
  4332. "a": 1
  4333. }, None],
  4334. "key": "val",
  4335. },
  4336. "tags": [
  4337. {
  4338. "value": "win",
  4339. "key": "builder"
  4340. },
  4341. {
  4342. "value": "git_cl_try",
  4343. "key": "user_agent"
  4344. },
  4345. ],
  4346. "gitilesCommit": {
  4347. "host": "chromium.googlesource.com",
  4348. "project": "depot_tools",
  4349. "id": "beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef",
  4350. "ref": "refs/heads/main",
  4351. }
  4352. },
  4353. }],
  4354. }
  4355. mockCallBuildbucket.assert_called_with(mock.ANY,
  4356. 'cr-buildbucket.appspot.com',
  4357. 'Batch', expected_request)
  4358. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4359. 'not supported in non-git environment')
  4360. @mock.patch('sys.stderr', io.StringIO())
  4361. def testScheduleOnBuildbucket_WrongBucket(self):
  4362. with self.assertRaises(SystemExit):
  4363. git_cl.main([
  4364. 'try', '-B', 'not-a-bucket', '-b', 'win', '-p', 'key=val', '-p',
  4365. 'json=[{"a":1}, null]'
  4366. ])
  4367. self.assertIn('Invalid bucket: not-a-bucket.', sys.stderr.getvalue())
  4368. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4369. 'not supported in non-git environment')
  4370. @mock.patch('git_cl._call_buildbucket')
  4371. @mock.patch('git_cl._fetch_tryjobs')
  4372. def testScheduleOnBuildbucketRetryFailed(self, mockFetchTryJobs,
  4373. mockCallBuildbucket):
  4374. git_cl._fetch_tryjobs.side_effect = lambda *_, **kw: {
  4375. 7: [],
  4376. 6: [{
  4377. 'id': 112112,
  4378. 'builder': {
  4379. 'project': 'chromium',
  4380. 'bucket': 'try',
  4381. 'builder': 'linux',
  4382. },
  4383. 'createTime': '2019-10-09T08:00:01.854286Z',
  4384. 'tags': [],
  4385. 'status': 'FAILURE',
  4386. }],
  4387. }[kw['patchset']]
  4388. mockCallBuildbucket.return_value = {}
  4389. self.assertEqual(0, git_cl.main(['try', '--retry-failed']))
  4390. self.assertIn('Scheduling jobs on:\n'
  4391. ' chromium/try: linux', git_cl.sys.stdout.getvalue())
  4392. expected_request = {
  4393. "requests": [{
  4394. "scheduleBuild": {
  4395. "requestId":
  4396. "uuid4",
  4397. "builder": {
  4398. "project": "chromium",
  4399. "bucket": "try",
  4400. "builder": "linux",
  4401. },
  4402. "gerritChanges": [{
  4403. "project": "depot_tools",
  4404. "host": "chromium-review.googlesource.com",
  4405. "patchset": 7,
  4406. "change": 123456,
  4407. }],
  4408. "properties": {
  4409. "category": "git_cl_try",
  4410. },
  4411. "tags": [
  4412. {
  4413. "value": "linux",
  4414. "key": "builder"
  4415. },
  4416. {
  4417. "value": "git_cl_try",
  4418. "key": "user_agent"
  4419. },
  4420. {
  4421. "value": "1",
  4422. "key": "retry_failed"
  4423. },
  4424. ],
  4425. },
  4426. }],
  4427. }
  4428. mockCallBuildbucket.assert_called_with(mock.ANY,
  4429. 'cr-buildbucket.appspot.com',
  4430. 'Batch', expected_request)
  4431. def test_parse_bucket(self):
  4432. test_cases = [
  4433. {
  4434. 'bucket': 'chromium/try',
  4435. 'result': ('chromium', 'try'),
  4436. },
  4437. {
  4438. 'bucket': 'luci.chromium.try',
  4439. 'result': ('chromium', 'try'),
  4440. 'has_warning': True,
  4441. },
  4442. {
  4443. 'bucket': 'skia.primary',
  4444. 'result': ('skia', 'skia.primary'),
  4445. 'has_warning': True,
  4446. },
  4447. {
  4448. 'bucket': 'not-a-bucket',
  4449. 'result': (None, None),
  4450. },
  4451. ]
  4452. for test_case in test_cases:
  4453. git_cl.sys.stdout.truncate(0)
  4454. self.assertEqual(test_case['result'],
  4455. git_cl._parse_bucket(test_case['bucket']))
  4456. if test_case.get('has_warning'):
  4457. expected_warning = 'WARNING Please use %s/%s to specify the bucket' % (
  4458. test_case['result'])
  4459. self.assertIn(expected_warning, git_cl.sys.stdout.getvalue())
  4460. class CMDUploadTestCase(CMDTestCaseBase):
  4461. def setUp(self):
  4462. super(CMDUploadTestCase, self).setUp()
  4463. mock.patch('git_cl._fetch_tryjobs').start()
  4464. mock.patch('git_cl._trigger_tryjobs', return_value={}).start()
  4465. mock.patch('git_cl.Changelist.CMDUpload', return_value=0).start()
  4466. mock.patch('git_cl.Settings.GetRoot', return_value='').start()
  4467. mock.patch('git_cl.Settings.GetSquashGerritUploads',
  4468. return_value=True).start()
  4469. self.addCleanup(mock.patch.stopall)
  4470. class MakeRequestsHelperTestCase(unittest.TestCase):
  4471. def exampleGerritChange(self):
  4472. return {
  4473. 'host': 'chromium-review.googlesource.com',
  4474. 'project': 'depot_tools',
  4475. 'change': 1,
  4476. 'patchset': 2,
  4477. }
  4478. def testMakeRequestsHelperNoOptions(self):
  4479. # Basic test for the helper function _make_tryjob_schedule_requests;
  4480. # it shouldn't throw AttributeError even when options doesn't have any
  4481. # of the expected values; it will use default option values.
  4482. changelist = ChangelistMock(gerrit_change=self.exampleGerritChange())
  4483. jobs = [('chromium', 'try', 'my-builder')]
  4484. options = optparse.Values()
  4485. requests = git_cl._make_tryjob_schedule_requests(changelist,
  4486. jobs,
  4487. options,
  4488. patchset=None)
  4489. # requestId is non-deterministic. Just assert that it's there and has
  4490. # a particular length.
  4491. self.assertEqual(len(requests[0]['scheduleBuild'].pop('requestId')), 36)
  4492. self.assertEqual(requests, [{
  4493. 'scheduleBuild': {
  4494. 'builder': {
  4495. 'bucket': 'try',
  4496. 'builder': 'my-builder',
  4497. 'project': 'chromium'
  4498. },
  4499. 'gerritChanges': [self.exampleGerritChange()],
  4500. 'properties': {
  4501. 'category': 'git_cl_try'
  4502. },
  4503. 'tags': [{
  4504. 'key': 'builder',
  4505. 'value': 'my-builder'
  4506. }, {
  4507. 'key': 'user_agent',
  4508. 'value': 'git_cl_try'
  4509. }]
  4510. }
  4511. }])
  4512. def testMakeRequestsHelperPresubmitSetsDryRunProperty(self):
  4513. changelist = ChangelistMock(gerrit_change=self.exampleGerritChange())
  4514. jobs = [('chromium', 'try', 'presubmit')]
  4515. options = optparse.Values()
  4516. requests = git_cl._make_tryjob_schedule_requests(changelist,
  4517. jobs,
  4518. options,
  4519. patchset=None)
  4520. self.assertEqual(requests[0]['scheduleBuild']['properties'], {
  4521. 'category': 'git_cl_try',
  4522. 'dry_run': 'true'
  4523. })
  4524. def testMakeRequestsHelperRevisionSet(self):
  4525. # Gitiles commit is specified when revision is in options.
  4526. changelist = ChangelistMock(gerrit_change=self.exampleGerritChange())
  4527. jobs = [('chromium', 'try', 'my-builder')]
  4528. options = optparse.Values({'revision': 'ba5eba11'})
  4529. requests = git_cl._make_tryjob_schedule_requests(changelist,
  4530. jobs,
  4531. options,
  4532. patchset=None)
  4533. self.assertEqual(
  4534. requests[0]['scheduleBuild']['gitilesCommit'], {
  4535. 'host': 'chromium.googlesource.com',
  4536. 'id': 'ba5eba11',
  4537. 'project': 'depot_tools',
  4538. 'ref': 'refs/heads/main',
  4539. })
  4540. def testMakeRequestsHelperRetryFailedSet(self):
  4541. # An extra tag is added when retry_failed is in options.
  4542. changelist = ChangelistMock(gerrit_change=self.exampleGerritChange())
  4543. jobs = [('chromium', 'try', 'my-builder')]
  4544. options = optparse.Values({'retry_failed': 'true'})
  4545. requests = git_cl._make_tryjob_schedule_requests(changelist,
  4546. jobs,
  4547. options,
  4548. patchset=None)
  4549. self.assertEqual(requests[0]['scheduleBuild']['tags'],
  4550. [{
  4551. 'key': 'builder',
  4552. 'value': 'my-builder'
  4553. }, {
  4554. 'key': 'user_agent',
  4555. 'value': 'git_cl_try'
  4556. }, {
  4557. 'key': 'retry_failed',
  4558. 'value': '1'
  4559. }])
  4560. def testMakeRequestsHelperCategorySet(self):
  4561. # The category property can be overridden with options.
  4562. changelist = ChangelistMock(gerrit_change=self.exampleGerritChange())
  4563. jobs = [('chromium', 'try', 'my-builder')]
  4564. options = optparse.Values({'category': 'my-special-category'})
  4565. requests = git_cl._make_tryjob_schedule_requests(changelist,
  4566. jobs,
  4567. options,
  4568. patchset=None)
  4569. self.assertEqual(requests[0]['scheduleBuild']['properties'],
  4570. {'category': 'my-special-category'})
  4571. class CMDFormatTestCase(unittest.TestCase):
  4572. def setUp(self):
  4573. super(CMDFormatTestCase, self).setUp()
  4574. mock.patch('git_cl.RunCommand').start()
  4575. mock.patch('clang_format.FindClangFormatToolInChromiumTree').start()
  4576. mock.patch('clang_format.FindClangFormatScriptInChromiumTree').start()
  4577. mock.patch('git_cl.settings').start()
  4578. self._top_dir = tempfile.mkdtemp()
  4579. self.addCleanup(mock.patch.stopall)
  4580. def tearDown(self):
  4581. shutil.rmtree(self._top_dir)
  4582. super(CMDFormatTestCase, self).tearDown()
  4583. def _make_temp_file(self, fname, contents):
  4584. gclient_utils.FileWrite(os.path.join(self._top_dir, fname),
  4585. ('\n'.join(contents)))
  4586. def _make_yapfignore(self, contents):
  4587. self._make_temp_file('.yapfignore', contents)
  4588. def _check_yapf_filtering(self, files, expected):
  4589. self.assertEqual(
  4590. expected,
  4591. git_cl._FilterYapfIgnoredFiles(
  4592. files, git_cl._GetYapfIgnorePatterns(self._top_dir)))
  4593. def _run_command_mock(self, return_value):
  4594. def f(*args, **kwargs):
  4595. if 'stdin' in kwargs:
  4596. self.assertIsInstance(kwargs['stdin'], bytes)
  4597. return return_value
  4598. return f
  4599. def testClangFormatDiffFull(self):
  4600. self._make_temp_file('test.cc', ['// test'])
  4601. git_cl.settings.GetFormatFullByDefault.return_value = False
  4602. diff_file = [os.path.join(self._top_dir, 'test.cc')]
  4603. mock_opts = mock.Mock(full=True, dry_run=True, diff=False)
  4604. # Diff
  4605. git_cl.RunCommand.side_effect = self._run_command_mock(' // test')
  4606. return_value = git_cl._RunClangFormatDiff(mock_opts, diff_file,
  4607. self._top_dir, 'HEAD')
  4608. self.assertEqual(2, return_value)
  4609. # No diff
  4610. git_cl.RunCommand.side_effect = self._run_command_mock('// test')
  4611. return_value = git_cl._RunClangFormatDiff(mock_opts, diff_file,
  4612. self._top_dir, 'HEAD')
  4613. self.assertEqual(0, return_value)
  4614. def testClangFormatDiff(self):
  4615. git_cl.settings.GetFormatFullByDefault.return_value = False
  4616. # A valid file is required, so use this test.
  4617. clang_format.FindClangFormatToolInChromiumTree.return_value = __file__
  4618. mock_opts = mock.Mock(full=False, dry_run=True, diff=False)
  4619. # Diff
  4620. git_cl.RunCommand.side_effect = self._run_command_mock('error')
  4621. return_value = git_cl._RunClangFormatDiff(mock_opts, ['.'],
  4622. self._top_dir, 'HEAD')
  4623. self.assertEqual(2, return_value)
  4624. # No diff
  4625. git_cl.RunCommand.side_effect = self._run_command_mock('')
  4626. return_value = git_cl._RunClangFormatDiff(mock_opts, ['.'],
  4627. self._top_dir, 'HEAD')
  4628. self.assertEqual(0, return_value)
  4629. def testYapfignoreExplicit(self):
  4630. self._make_yapfignore(['foo/bar.py', 'foo/bar/baz.py'])
  4631. files = [
  4632. 'bar.py',
  4633. 'foo/bar.py',
  4634. 'foo/baz.py',
  4635. 'foo/bar/baz.py',
  4636. 'foo/bar/foobar.py',
  4637. ]
  4638. expected = [
  4639. 'bar.py',
  4640. 'foo/baz.py',
  4641. 'foo/bar/foobar.py',
  4642. ]
  4643. self._check_yapf_filtering(files, expected)
  4644. def testYapfignoreSingleWildcards(self):
  4645. self._make_yapfignore(['*bar.py', 'foo*', 'baz*.py'])
  4646. files = [
  4647. 'bar.py', # Matched by *bar.py.
  4648. 'bar.txt',
  4649. 'foobar.py', # Matched by *bar.py, foo*.
  4650. 'foobar.txt', # Matched by foo*.
  4651. 'bazbar.py', # Matched by *bar.py, baz*.py.
  4652. 'bazbar.txt',
  4653. 'foo/baz.txt', # Matched by foo*.
  4654. 'bar/bar.py', # Matched by *bar.py.
  4655. 'baz/foo.py', # Matched by baz*.py, foo*.
  4656. 'baz/foo.txt',
  4657. ]
  4658. expected = [
  4659. 'bar.txt',
  4660. 'bazbar.txt',
  4661. 'baz/foo.txt',
  4662. ]
  4663. self._check_yapf_filtering(files, expected)
  4664. def testYapfignoreMultiplewildcards(self):
  4665. self._make_yapfignore(['*bar*', '*foo*baz.txt'])
  4666. files = [
  4667. 'bar.py', # Matched by *bar*.
  4668. 'bar.txt', # Matched by *bar*.
  4669. 'abar.py', # Matched by *bar*.
  4670. 'foobaz.txt', # Matched by *foo*baz.txt.
  4671. 'foobaz.py',
  4672. 'afoobaz.txt', # Matched by *foo*baz.txt.
  4673. ]
  4674. expected = [
  4675. 'foobaz.py',
  4676. ]
  4677. self._check_yapf_filtering(files, expected)
  4678. def testYapfignoreComments(self):
  4679. self._make_yapfignore(['test.py', '#test2.py'])
  4680. files = [
  4681. 'test.py',
  4682. 'test2.py',
  4683. ]
  4684. expected = [
  4685. 'test2.py',
  4686. ]
  4687. self._check_yapf_filtering(files, expected)
  4688. def testYapfHandleUtf8(self):
  4689. self._make_yapfignore(['test.py', 'test_🌐.py'])
  4690. files = [
  4691. 'test.py',
  4692. 'test_🌐.py',
  4693. 'test2.py',
  4694. ]
  4695. expected = [
  4696. 'test2.py',
  4697. ]
  4698. self._check_yapf_filtering(files, expected)
  4699. def testYapfignoreBlankLines(self):
  4700. self._make_yapfignore(['test.py', '', '', 'test2.py'])
  4701. files = [
  4702. 'test.py',
  4703. 'test2.py',
  4704. 'test3.py',
  4705. ]
  4706. expected = [
  4707. 'test3.py',
  4708. ]
  4709. self._check_yapf_filtering(files, expected)
  4710. def testYapfignoreWhitespace(self):
  4711. self._make_yapfignore([' test.py '])
  4712. files = [
  4713. 'test.py',
  4714. 'test2.py',
  4715. ]
  4716. expected = [
  4717. 'test2.py',
  4718. ]
  4719. self._check_yapf_filtering(files, expected)
  4720. def testYapfignoreNoFiles(self):
  4721. self._make_yapfignore(['test.py'])
  4722. self._check_yapf_filtering([], [])
  4723. def testYapfignoreMissingYapfignore(self):
  4724. files = [
  4725. 'test.py',
  4726. ]
  4727. expected = [
  4728. 'test.py',
  4729. ]
  4730. self._check_yapf_filtering(files, expected)
  4731. @mock.patch('gclient_paths.GetPrimarySolutionPath')
  4732. def testRunMetricsXMLFormatSkipIfPresubmit(self, find_top_dir):
  4733. """Verifies that it skips the formatting if opts.presubmit is True."""
  4734. find_top_dir.return_value = self._top_dir
  4735. mock_opts = mock.Mock(full=True,
  4736. dry_run=True,
  4737. diff=False,
  4738. presubmit=True)
  4739. files = [
  4740. os.path.join(self._top_dir, 'tools', 'metrics', 'ukm', 'ukm.xml'),
  4741. ]
  4742. return_value = git_cl._RunMetricsXMLFormat(mock_opts, files,
  4743. self._top_dir, 'HEAD')
  4744. git_cl.RunCommand.assert_not_called()
  4745. self.assertEqual(0, return_value)
  4746. @mock.patch('gclient_paths.GetPrimarySolutionPath')
  4747. def testRunMetricsFormatWithUkm(self, find_top_dir):
  4748. """Checks if the command line arguments do not contain the input path.
  4749. """
  4750. find_top_dir.return_value = self._top_dir
  4751. mock_opts = mock.Mock(full=True,
  4752. dry_run=False,
  4753. diff=False,
  4754. presubmit=False)
  4755. files = [
  4756. os.path.join(self._top_dir, 'tools', 'metrics', 'ukm', 'ukm.xml'),
  4757. ]
  4758. git_cl._RunMetricsXMLFormat(mock_opts, files, self._top_dir, 'HEAD')
  4759. git_cl.RunCommand.assert_called_with([
  4760. mock.ANY,
  4761. os.path.join(self._top_dir, 'tools', 'metrics', 'ukm',
  4762. 'pretty_print.py'),
  4763. '--non-interactive',
  4764. ],
  4765. cwd=self._top_dir)
  4766. @mock.patch('gclient_paths.GetPrimarySolutionPath')
  4767. def testRunMetricsFormatWithHistograms(self, find_top_dir):
  4768. """Checks if the command line arguments contain the input file paths."""
  4769. find_top_dir.return_value = self._top_dir
  4770. mock_opts = mock.Mock(full=True,
  4771. dry_run=False,
  4772. diff=False,
  4773. presubmit=False)
  4774. files = [
  4775. os.path.join(self._top_dir, 'tools', 'metrics', 'histograms',
  4776. 'enums.xml'),
  4777. os.path.join(self._top_dir, 'tools', 'metrics', 'histograms',
  4778. 'test_data', 'enums.xml'),
  4779. ]
  4780. git_cl._RunMetricsXMLFormat(mock_opts, files, self._top_dir, 'HEAD')
  4781. pretty_print_path = os.path.join(self._top_dir, 'tools', 'metrics',
  4782. 'histograms', 'pretty_print.py')
  4783. git_cl.RunCommand.assert_has_calls([
  4784. mock.call(
  4785. [mock.ANY, pretty_print_path, '--non-interactive', files[0]],
  4786. cwd=self._top_dir),
  4787. mock.call(
  4788. [mock.ANY, pretty_print_path, '--non-interactive', files[1]],
  4789. cwd=self._top_dir),
  4790. ])
  4791. @mock.patch('subprocess2.call')
  4792. def testLUCICfgFormatWorks(self, mock_call):
  4793. """Checks if lucicfg is given then input file path."""
  4794. mock_opts = mock.Mock(dry_run=False)
  4795. files = ['test/main.star']
  4796. mock_call.return_value = 0
  4797. ret = git_cl._RunLUCICfgFormat(mock_opts, files, self._top_dir, 'HEAD')
  4798. mock_call.assert_called_with([
  4799. mock.ANY,
  4800. 'fmt',
  4801. 'test/main.star',
  4802. ])
  4803. self.assertEqual(ret, 0)
  4804. @mock.patch('subprocess2.call')
  4805. def testLUCICfgFormatWithDryRun(self, mock_call):
  4806. """Tests the command with --dry-run."""
  4807. mock_opts = mock.Mock(dry_run=True)
  4808. files = ['test/main.star']
  4809. git_cl._RunLUCICfgFormat(mock_opts, files, self._top_dir, 'HEAD')
  4810. mock_call.assert_called_with([
  4811. mock.ANY,
  4812. 'fmt',
  4813. '--dry-run',
  4814. 'test/main.star',
  4815. ])
  4816. @mock.patch('subprocess2.call')
  4817. def testLUCICfgFormatWithDryRunReturnCode(self, mock_call):
  4818. """Tests that it returns 2 for non-zero exit codes."""
  4819. mock_opts = mock.Mock(dry_run=True)
  4820. files = ['test/main.star']
  4821. run = git_cl._RunLUCICfgFormat
  4822. mock_call.return_value = 0
  4823. self.assertEqual(run(mock_opts, files, self._top_dir, 'HEAD'), 0)
  4824. mock_call.return_value = 1
  4825. self.assertEqual(run(mock_opts, files, self._top_dir, 'HEAD'), 2)
  4826. mock_call.return_value = 2
  4827. self.assertEqual(run(mock_opts, files, self._top_dir, 'HEAD'), 2)
  4828. mock_call.return_value = 255
  4829. self.assertEqual(run(mock_opts, files, self._top_dir, 'HEAD'), 2)
  4830. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4831. 'not supported in non-git environment')
  4832. class CMDStatusTestCase(CMDTestCaseBase):
  4833. # Return branch names a,..,f with comitterdates in increasing order, i.e.
  4834. # 'f' is the most-recently changed branch.
  4835. def _mock_run_git(commands):
  4836. if commands == [
  4837. 'for-each-ref', '--format=%(refname) %(committerdate:unix)',
  4838. 'refs/heads'
  4839. ]:
  4840. branches_and_committerdates = [
  4841. 'refs/heads/a 1',
  4842. 'refs/heads/b 2',
  4843. 'refs/heads/c 3',
  4844. 'refs/heads/d 4',
  4845. 'refs/heads/e 5',
  4846. 'refs/heads/f 6',
  4847. ]
  4848. return '\n'.join(branches_and_committerdates)
  4849. # Mock the status in such a way that the issue number gives us an
  4850. # indication of the commit date (simplifies manual debugging).
  4851. def _mock_get_cl_statuses(branches, fine_grained, max_processes):
  4852. for c in branches:
  4853. c.issue = (100 + int(c.GetCommitDate()))
  4854. yield (c, 'open')
  4855. @mock.patch('git_cl.Changelist.EnsureAuthenticated')
  4856. @mock.patch('git_cl.Changelist.FetchDescription', lambda cl, pretty: 'x')
  4857. @mock.patch('git_cl.Changelist.GetIssue', lambda cl: cl.issue)
  4858. @mock.patch('git_cl.RunGit', _mock_run_git)
  4859. @mock.patch('git_cl.get_cl_statuses', _mock_get_cl_statuses)
  4860. @mock.patch('git_cl.Settings.GetRoot', return_value='')
  4861. @mock.patch('git_cl.Settings.IsStatusCommitOrderByDate', return_value=False)
  4862. @mock.patch('scm.GIT.GetBranch', return_value='a')
  4863. def testStatus(self, *_mocks):
  4864. self.assertEqual(0, git_cl.main(['status', '--no-branch-color']))
  4865. self.maxDiff = None
  4866. self.assertEqual(
  4867. sys.stdout.getvalue(), 'Branches associated with reviews:\n'
  4868. ' * a : https://crrev.com/c/101 (open)\n'
  4869. ' b : https://crrev.com/c/102 (open)\n'
  4870. ' c : https://crrev.com/c/103 (open)\n'
  4871. ' d : https://crrev.com/c/104 (open)\n'
  4872. ' e : https://crrev.com/c/105 (open)\n'
  4873. ' f : https://crrev.com/c/106 (open)\n\n'
  4874. 'Current branch: a\n'
  4875. 'Issue number: 101 (https://chromium-review.googlesource.com/101)\n'
  4876. 'Issue description:\n'
  4877. 'x\n')
  4878. @mock.patch('git_cl.Changelist.EnsureAuthenticated')
  4879. @mock.patch('git_cl.Changelist.FetchDescription', lambda cl, pretty: 'x')
  4880. @mock.patch('git_cl.Changelist.GetIssue', lambda cl: cl.issue)
  4881. @mock.patch('git_cl.RunGit', _mock_run_git)
  4882. @mock.patch('git_cl.get_cl_statuses', _mock_get_cl_statuses)
  4883. @mock.patch('git_cl.Settings.GetRoot', return_value='')
  4884. @mock.patch('git_cl.Settings.IsStatusCommitOrderByDate', return_value=False)
  4885. @mock.patch('scm.GIT.GetBranch', return_value='a')
  4886. def testStatusByDate(self, *_mocks):
  4887. self.assertEqual(
  4888. 0, git_cl.main(['status', '--no-branch-color', '--date-order']))
  4889. self.maxDiff = None
  4890. self.assertEqual(
  4891. sys.stdout.getvalue(), 'Branches associated with reviews:\n'
  4892. ' f : https://crrev.com/c/106 (open)\n'
  4893. ' e : https://crrev.com/c/105 (open)\n'
  4894. ' d : https://crrev.com/c/104 (open)\n'
  4895. ' c : https://crrev.com/c/103 (open)\n'
  4896. ' b : https://crrev.com/c/102 (open)\n'
  4897. ' * a : https://crrev.com/c/101 (open)\n\n'
  4898. 'Current branch: a\n'
  4899. 'Issue number: 101 (https://chromium-review.googlesource.com/101)\n'
  4900. 'Issue description:\n'
  4901. 'x\n')
  4902. @mock.patch('git_cl.Changelist.EnsureAuthenticated')
  4903. @mock.patch('git_cl.Changelist.FetchDescription', lambda cl, pretty: 'x')
  4904. @mock.patch('git_cl.Changelist.GetIssue', lambda cl: cl.issue)
  4905. @mock.patch('git_cl.RunGit', _mock_run_git)
  4906. @mock.patch('git_cl.get_cl_statuses', _mock_get_cl_statuses)
  4907. @mock.patch('git_cl.Settings.GetRoot', return_value='')
  4908. @mock.patch('git_cl.Settings.IsStatusCommitOrderByDate', return_value=True)
  4909. @mock.patch('scm.GIT.GetBranch', return_value='a')
  4910. def testStatusByDate(self, *_mocks):
  4911. self.assertEqual(0, git_cl.main(['status', '--no-branch-color']))
  4912. self.maxDiff = None
  4913. self.assertEqual(
  4914. sys.stdout.getvalue(), 'Branches associated with reviews:\n'
  4915. ' f : https://crrev.com/c/106 (open)\n'
  4916. ' e : https://crrev.com/c/105 (open)\n'
  4917. ' d : https://crrev.com/c/104 (open)\n'
  4918. ' c : https://crrev.com/c/103 (open)\n'
  4919. ' b : https://crrev.com/c/102 (open)\n'
  4920. ' * a : https://crrev.com/c/101 (open)\n\n'
  4921. 'Current branch: a\n'
  4922. 'Issue number: 101 (https://chromium-review.googlesource.com/101)\n'
  4923. 'Issue description:\n'
  4924. 'x\n')
  4925. @unittest.skipIf(gclient_utils.IsEnvCog(),
  4926. 'not supported in non-git environment')
  4927. class CMDOwnersTestCase(CMDTestCaseBase):
  4928. def setUp(self):
  4929. super(CMDOwnersTestCase, self).setUp()
  4930. self.owners_by_path = {
  4931. 'foo': ['a@example.com'],
  4932. 'bar': ['b@example.com', 'c@example.com'],
  4933. }
  4934. mock.patch('git_cl.Settings.GetRoot', return_value='root').start()
  4935. mock.patch('git_cl.Changelist.GetAuthor', return_value='author').start()
  4936. mock.patch('git_cl.Changelist.GetAffectedFiles',
  4937. return_value=list(self.owners_by_path)).start()
  4938. mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream',
  4939. return_value='upstream').start()
  4940. mock.patch('git_cl.Changelist.GetGerritHost',
  4941. return_value='host').start()
  4942. mock.patch('git_cl.Changelist.GetGerritProject',
  4943. return_value='project').start()
  4944. mock.patch('git_cl.Changelist.GetRemoteBranch',
  4945. return_value=('origin', 'refs/remotes/origin/main')).start()
  4946. mock.patch('owners_client.OwnersClient.BatchListOwners',
  4947. return_value=self.owners_by_path).start()
  4948. mock.patch('gerrit_util.IsCodeOwnersEnabledOnHost',
  4949. return_value=True).start()
  4950. self.addCleanup(mock.patch.stopall)
  4951. def testShowAllNoArgs(self):
  4952. self.assertEqual(0, git_cl.main(['owners', '--show-all']))
  4953. self.assertEqual('No files specified for --show-all. Nothing to do.\n',
  4954. git_cl.sys.stdout.getvalue())
  4955. def testShowAll(self):
  4956. self.assertEqual(
  4957. 0, git_cl.main(['owners', '--show-all', 'foo', 'bar', 'baz']))
  4958. owners_client.OwnersClient.BatchListOwners.assert_called_once_with(
  4959. ['foo', 'bar', 'baz'])
  4960. self.assertEqual(
  4961. '\n'.join([
  4962. 'Owners for foo:',
  4963. ' - a@example.com',
  4964. 'Owners for bar:',
  4965. ' - b@example.com',
  4966. ' - c@example.com',
  4967. 'Owners for baz:',
  4968. ' - No owners found',
  4969. '',
  4970. ]), sys.stdout.getvalue())
  4971. def testBatch(self):
  4972. self.assertEqual(0, git_cl.main(['owners', '--batch']))
  4973. self.assertIn('a@example.com', sys.stdout.getvalue())
  4974. self.assertIn('b@example.com', sys.stdout.getvalue())
  4975. class CMDLintTestCase(CMDTestCaseBase):
  4976. bad_indent = '\n'.join([
  4977. '// Copyright 1999 <a@example.com>',
  4978. 'namespace foo {',
  4979. ' class a;',
  4980. '}',
  4981. '',
  4982. ])
  4983. filesInCL = ['foo', 'bar']
  4984. def setUp(self):
  4985. super(CMDLintTestCase, self).setUp()
  4986. mock.patch('git_cl.sys.stderr', io.StringIO()).start()
  4987. mock.patch('codecs.open', mock.mock_open()).start()
  4988. mock.patch('os.path.isfile', return_value=True).start()
  4989. def testLintSingleFile(self, *_mock):
  4990. codecs.open().read.return_value = self.bad_indent
  4991. self.assertEqual(1, git_cl.main(['lint', 'pdf.h']))
  4992. self.assertIn('pdf.h:3: (cpplint) Do not indent within a namespace',
  4993. git_cl.sys.stderr.getvalue())
  4994. def testLintMultiFiles(self, *_mock):
  4995. codecs.open().read.return_value = self.bad_indent
  4996. self.assertEqual(1, git_cl.main(['lint', 'pdf.h', 'pdf.cc']))
  4997. self.assertIn('pdf.h:3: (cpplint) Do not indent within a namespace',
  4998. git_cl.sys.stderr.getvalue())
  4999. self.assertIn('pdf.cc:3: (cpplint) Do not indent within a namespace',
  5000. git_cl.sys.stderr.getvalue())
  5001. @unittest.skipIf(gclient_utils.IsEnvCog(),
  5002. 'not supported in non-git environment')
  5003. @mock.patch('git_cl.Changelist.GetAffectedFiles',
  5004. return_value=['chg-1.h', 'chg-2.cc'])
  5005. @mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream',
  5006. return_value='upstream')
  5007. @mock.patch('git_cl.Settings.GetRoot', return_value='.')
  5008. @mock.patch('git_cl.FindCodereviewSettingsFile', return_value=None)
  5009. def testLintChangelist(self, *_mock):
  5010. codecs.open().read.return_value = self.bad_indent
  5011. self.assertEqual(1, git_cl.main(['lint']))
  5012. self.assertIn('chg-1.h:3: (cpplint) Do not indent within a namespace',
  5013. git_cl.sys.stderr.getvalue())
  5014. self.assertIn('chg-2.cc:3: (cpplint) Do not indent within a namespace',
  5015. git_cl.sys.stderr.getvalue())
  5016. class CMDCherryPickTestCase(CMDTestCaseBase):
  5017. def setUp(self):
  5018. super(CMDTestCaseBase, self).setUp()
  5019. def testCreateCommitMessage(self):
  5020. orig_message = """Foo the bar
  5021. This change foo's the bar.
  5022. Bug: 123456
  5023. Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
  5024. """
  5025. expected_message = """Cherry pick "Foo the bar"
  5026. Original change's description:
  5027. > Foo the bar
  5028. >
  5029. > This change foo's the bar.
  5030. >
  5031. > Bug: 123456
  5032. > Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
  5033. Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
  5034. """
  5035. self.assertEqual(git_cl._create_commit_message(orig_message),
  5036. expected_message)
  5037. def testCreateCommitMessageWithBug(self):
  5038. bug = "987654"
  5039. orig_message = """Foo the bar
  5040. This change foo's the bar.
  5041. Bug: 123456
  5042. Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
  5043. """
  5044. expected_message = f"""Cherry pick "Foo the bar"
  5045. Original change's description:
  5046. > Foo the bar
  5047. >
  5048. > This change foo's the bar.
  5049. >
  5050. > Bug: 123456
  5051. > Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
  5052. Bug: {bug}
  5053. Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
  5054. """
  5055. self.assertEqual(git_cl._create_commit_message(orig_message, bug),
  5056. expected_message)
  5057. @unittest.skipIf(gclient_utils.IsEnvCog(),
  5058. 'not supported in non-git environment')
  5059. class CMDSplitTestCase(CMDTestCaseBase):
  5060. def setUp(self):
  5061. super(CMDTestCaseBase, self).setUp()
  5062. mock.patch('git_cl.Settings.GetRoot', return_value='root').start()
  5063. @mock.patch("split_cl.SplitCl", return_value=0)
  5064. @mock.patch("git_cl.OptionParser.error", side_effect=ParserErrorMock)
  5065. def testDescriptionFlagRequired(self, _, mock_split_cl):
  5066. # --description-file is mandatory...
  5067. self.assertRaises(ParserErrorMock, git_cl.main, ['split'])
  5068. self.assertEqual(mock_split_cl.call_count, 0)
  5069. self.assertEqual(git_cl.main(['split', '--description=SomeFile.txt']),
  5070. 0)
  5071. self.assertEqual(mock_split_cl.call_count, 1)
  5072. # Unless we're doing a dry run
  5073. mock_split_cl.reset_mock()
  5074. self.assertEqual(git_cl.main(['split', '-n']), 0)
  5075. self.assertEqual(mock_split_cl.call_count, 1)
  5076. if __name__ == '__main__':
  5077. logging.basicConfig(
  5078. level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)
  5079. unittest.main()