From a32975e7725a7fe441c37a5f9b89d6b573451cd4 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 19 Feb 2019 20:44:23 +0100 Subject: [PATCH] Add actorbintree assignment --- .gitignore | 16 + .gitlab-ci.yml | 36 ++ .vscode/settings.json | 8 + assignment.sbt | 4 + build.sbt | 24 ++ grading-tests.jar | Bin 0 -> 20878 bytes project/MOOCSettings.scala | 46 +++ project/StudentTasks.scala | 318 ++++++++++++++++++ project/build.properties | 1 + project/buildSettings.sbt | 5 + project/plugins.sbt | 2 + .../scala/actorbintree/BinaryTreeSet.scala | 118 +++++++ .../scala/actorbintree/BinaryTreeSuite.scala | 126 +++++++ 13 files changed, 704 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .vscode/settings.json create mode 100644 assignment.sbt create mode 100644 build.sbt create mode 100644 grading-tests.jar create mode 100644 project/MOOCSettings.scala create mode 100644 project/StudentTasks.scala create mode 100644 project/build.properties create mode 100644 project/buildSettings.sbt create mode 100644 project/plugins.sbt create mode 100644 src/main/scala/actorbintree/BinaryTreeSet.scala create mode 100644 src/test/scala/actorbintree/BinaryTreeSuite.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..349d2e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# Dotty IDE +/.dotty-ide-artifact +/.dotty-ide.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..c307fad --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,36 @@ +# DO NOT EDIT THIS FILE + +stages: + - build + - grade + +compile: + stage: build + image: lampepfl/moocs:dotty-2020-02-12 + except: + - tags + tags: + - cs206 + script: + - sbt packageSubmission + artifacts: + expire_in: 1 day + paths: + - submission.jar + +grade: + stage: grade + except: + - tags + tags: + - cs206 + image: + name: smarter3/moocs:reactive-actorbintree-2020-04-15 + entrypoint: [""] + allow_failure: true + before_script: + - mkdir -p /shared/submission/ + - cp submission.jar /shared/submission/submission.jar + script: + - cd /grader + - /grader/grade | /grader/feedback-printer diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a35362b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "dotty": { + "trace": { + "remoteTracingUrl": "wss://lamppc36.epfl.ch/dotty-remote-tracer/upload/lsp.log", + "server": { "format": "JSON", "verbosity": "verbose" } + } + } +} diff --git a/assignment.sbt b/assignment.sbt new file mode 100644 index 0000000..c6705a8 --- /dev/null +++ b/assignment.sbt @@ -0,0 +1,4 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) + + diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..e08d686 --- /dev/null +++ b/build.sbt @@ -0,0 +1,24 @@ +course := "reactive" +assignment := "actorbintree" + +testOptions in Test += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s") +parallelExecution in Test := false + +val akkaVersion = "2.6.0" + +scalaVersion := "0.23.0-bin-20200211-5b006fb-NIGHTLY" + +scalacOptions ++= Seq( + "-feature", + "-deprecation", + "-encoding", "UTF-8", + "-unchecked", + "-language:implicitConversions" +) + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-actor" % akkaVersion, + "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test, + "com.novocode" % "junit-interface" % "0.11" % Test +).map(_.withDottyCompat(scalaVersion.value)) +testSuite := "actorbintree.BinaryTreeSuite" diff --git a/grading-tests.jar b/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..3f08ec48eee19cc57a94ea73db973c534fcf89dc GIT binary patch literal 20878 zcmbTcQ;;T15GDGxZJX1!ZQHhOYucE$jcMDqrfu8&+IIK;yZf@S_uiLVFIg3FA}g~Z zvMQs_QIZ7%M+ZPdLjx$n;*|jZ$3O>w1LVckgz2RfBpBs|6{ID^Rn-{eCB7#BfCu@h zNjX`1hIs^8dYYN3*(MdHCDy&;`ziV{>1p|CT4_2ESYWtW+EK9%HF^mJMY>s92@tg6 zv#@xoJE?F<4gQ}~GW=tp#W~~$Hzc_4Ys9S!1XeMHWvMyEP|9)15!OB%x;k?dA|SK+ zB2%PzpNk-7SuZF2-US4pBnt&?!=n1*`(LQ?Ab|hYVBoL-05stLkpRMf2xC(>M;8-o z2R9dUbH@K~D8&DS+S(fbFUbGbmR)@G7~71%X1iEwAJ~lExggZJmXek>ZGFFiC~q-nCZek(1cD)^jAHIg z`^Oy%G`tv^zObC?&jAA|abqM^B(zS-?#tH8i8)24bm`aR&&%7>Vy5S=|4W|l_4;gw z)uO8kF&ACBJQ*F0p++sftgSAT>DC>s36`i#3Ng>CKODu?c}ulg zmBwVXmcuF(IC83udChL_o+BHk6&$oX#=qogOlXK89TgOPn^`B=Xm<-WDqXxu^Ztw+ z@Kd~HP@EE3sRWLv6HcpX1Eyq>N0{HXbxVPu7WVZ%KD-SE85LYz@fD?eZ(3b36=qtL zi;v30Ij#6j6f8JaOw-urX0qL*GN7Z-$*T^m)A+7@1&Zb@x!Lp9?X{aS5~>YiTg9fr zqAzI`wF%l)CU;u13ROeJnR17cZmi2hFzCbq_^Rh)-}BtSxLqT6&ujJySp4a0yxa-A z>e^xPxj#Be>kjb>=DzR=xnMNBoCS?5!QszhmSxKfw4BY_ShWjA&l_GAYN^t+_T8F?Nur~LrkKkz+*nkZObHjseN{BsTAGaYQCrC| zcR`NCbrj8MChwMt&E*JjKu74p>59{YtSc)ynF)&QQz*2DRNfpmH3@0ASI&T8 zDy`1H0}6T(^lB$KD}7WWHB@v1`I=F9t5R}7)?qD0944zQ?F*-c?-#U*Wo<0sPn>-& ztilPa?;o{tH-+me6S*AgTTO)$;roGbBU=2YkI1=_r*IOE5pxtZ9&o4-H?Gf2S_k?o zlxUmP;x$E)lqp72r-L7x@tE%i-NO@Xq|s@go_kh9U`E0BQw{D@!2HkcPJCPR!5OvD?KU(nHlZ5dDh!ZovR-9+osQQ}i7p z+Aw9!oV{71tJPg>RjcCcxmn#0%Fd^mHFbXUF;8yUt<3qj1>Tw5R9cJv{HU#}Wc3{wAZKi}&zojYuE;Va)wp;V`mnU>Be=#`V>cL_-kBb@WZ^!5}t?U2)|v z&JkVFNBj4I($ebIi%A)m&4on zxCq$!_;ZwjL&{qkL4-vzC9YLGBpe--o&0o@HO36kXG~3T(_9^Q30!kYYJ87s`N)!0 znitx4cR=}%g_LbB^CYTobsX!V3a%IY{J%_FT@DMXn_J<)N3O3O#l9lS{!+C?YP>EW<^8S+gUT$=o^g|T;JMSf1Q^L z)D@Y>#(!32?g7bzPGzr0{)(8vAo{j2*j!pr(I0~)ZJz?6vA`&F1uswMKVKssC6s#> zF`#J^MovIKtuO4bZloR-c!pX(0(6UzL~>)`lK>(<*@8xy+0|O?=&dxL&3f9B04L$X ze7dcxkARy)SM4ACMAUQ_obo9&ax(0$ts>{#vodQx_{D=FlV}8KGC~-@oS9uGvEhE2 z6&tGRmm8lV8VZ4b2g>5p#3&?FSFklARi@0d&2)?V)CQV3kIk`ayQ#}&uuQUL2YXKj&wwe;c!;)N&J0=}|kS}i*4m6`}uI5(>eZxv7LeE`n`#>cnle{f~d8oubxnTa- zv}t6P>d68c=0`=4qJWNa6Nxo27Zp&TDx686f$z}Z@5-{#!f&+1N$r~FX8^O4!2G?gq@d$cOVFN>?fbkrzw+%oix%#DAGyG}Ux%tDYD zYcZyQ3-QOf@+uNvu$e#gS#gujd9O2b$?a-vDbd@NHD$P8BEuq zzo168my0y@6B)?sQ-Y5vug!fBjDNW4A(p%;HvSYyvat5TUm?chGiP$v+{Bp$W@E*P z4$g9TCgrL=BO`7s<3gCvD0A;o`mh74*VLFHE}8d_>s#vcg(D3)k5@*nwHc1yOee!L zB}o_xbW0`z+1PTkKg~-f0=+8%u8k};LBv-~PovhjKpkroY-=1Q>d-^dl)C*RP6qr- zISL&YqGEo+Oca8pHzE08)z<|yi|D}M<4wGCs)v2bcgN-$@W$=ko}7$17XNC}D~5d^ zhsxN;TC~xkuGz&wgwh-gtEKA>63uuxDLX!NBZAxYYhHIgF>+Qx{ZUIpQ=CTG*tMZ> z;t!>d=Vf(meH8QPsHPNdHU+z?pP7al^eHSZ>wN}b!55=XlIwk^Ei3UMUS=BKkS@|- zdKFr87m2;ggO1&`EbJ|$M6HXWTA_+|n5e|^OQ{NP$7X{L z`D%usGLr9ij$=yhF1UAZrnPo&q9jFCK9#u0gLLWoomk~y+5{|bUCo;PuNv5O)wX5j zzy99;tS9|H&)c%#BQX^PTu_|RI8c)O{i+gxO~^^J%<8c;6^+o!M258%+NZEoe*qthy)w%O-RLB>BG(%OM zr4{KiT*m5-@Q~V)Fb!-^HEI-LrY?a%33z0u>@^%ROf#SByXOEO)dmIb@GB{|OusVx z#cJ4F{ZE9_h2m>>v?4<19LwSw^^xwaD2GWcWrEOq2SVf%gb9-s4`}1=Z5L>=XnNi( zDaTjbms?KrSl>*Bk6mInjkUYh;0jGA{#2xUlJ(s$r9ek!_c(QQdZ#aui{j5KmHRtS z@@;fWjWjR;RpqW4(L)~7dU}H3X+DzC=OO`UYNqtIS?v0H-MfUv=K1J5ZW|)OqlkZ| z-(&RRaZ+@00b+=)rdYZ>3N|ozif{A8KcT-N=UY!7FR#X>dViDr+Wha0P<&6#|2^I} zVluS-!V+29eK*{PaBEsvh_PAJpinPzfN*Np1d1~7tko!c(>FL8PgWqtX9nX5bnWFz zu*A6hfC*5+^!a058CqC`;HLxmofw7Z_esY9LL?|?H-NuflIUcB%=O#iz6AAo2>BGxW$~3`N2|&cWGtpR-w(_xiA&xbNyxI_ z)esJCc(I~n=~(ahGf!sS&R%SzRx?+Rr~;}?H!~7s{XT|8f!lZkvTeJO%S&9Z;8Qc|I>0FT38Ls%h?&eJ0qXWVTP|| zrYAKNh*;U)iM)V7m2Fy)i+hy-{2OsBOjAs=1iwEczBhASpQS7#Ow#VjOKRcmc+#%N zl3-dQyTl8=$BV-FJfi=lxDoWbNl_nD#dVoa`&?h^eA}44E=8nqVN7+2WWoluTha`s zf46D}iUldxn_+o$uCx zaQ_3-*D3CgI(b&0*WcfG!rd2^{?+_02S9r^hPvkV!c2?BvFB>khCmk$YfXJEZkZ$EH9hgDApKdFj8n)dyzx6L6=|@ds#G1ejHr z2X&x5x+xJLi)dR1dVSVcnasiv95m5T;#% z&C1`IAug(z6YaDpk}H`wg7}6?pMbeOmAZdFg?lTL5_tMqZv#>sYw6lXMYujyFcz*6~Z*Jfy;;uk5W`3jkxt3 zzc)6)xzgE$Iu%#Z-;8u$dji#O{v?;&raVZ(Tb{zAT`8 zA%87BhW%~}#l{t1v|aspEAg|= zqHna9L9?^<<_LW|5G&IPlKLK*4i`mzzcWb#1#i;1EfRhUEw=8wXl0@0t9jpnr^zC) zbt_20yt=h+7(pH)}Zp`2sX+iskcPHg-%i zs0;4^e!h@rUbDi`=F_xSU%Fq0pW=zUyGCOMYEq54cGy2P<6-PTqgu>#cUhwJY z9H`HqZ@>hIzUNTQV{_BK#(#EgC4#L%=N!i;jc;Z}@Q*X2FzRK^4_boRqrG?Q&W7Yq z`BmBPr};ix$govHML|5PL3%`&xq>VKhG*D40{EnQ|aehQuzENWm>qhxu$oL>J z==D&swskXB4c_4Q#_x*dDK=>@BqKQQeBj}_NNPZ>bfjUb?P1)1`w}GG28CSR6GfY)&8wSV0ysM)L#+LrzBV=& zWQ5h79+7!v$)5n3&~toPk**;(!ZQzn<;Bg4%)){z4y9It8pI7S8bcZ0zx>Sa==ws2 zh5E>!3h$Wi^L1w;@(&ONMgUyn3uBnCl+~d3KOUx^?%jkYZ*bKQ#?4}``4i)bZ5{a9 zuyJGUAFN!3QEn8GN3sthUS`D|1F}aercDLVp=q0>+1Cb^^{a>XVh-~0Ly-Xj7sx&B z6s{22aN(2S@slB>=@5%OovPSIHU{klD}M&YC|*&K!-QlCdx(dBG1cA_*!WO_zo{U9 zuteS-6Q6b&lhp-cbfaeZVQdGl*dyP7bZ)Twf+XxvxKLu7U3_m7YwEn*%t~+2Gg$B5 zBs@DYEQC+fZG_t{3@^Xr-A=yuj&(3Liu1GB9z1|B9fufAxU0d5Fa+Ol5qz7#nFUb7pAoL5BhAM? zGIzo({AS&TSlN=yFSwbfJXby&K76!($J@>tC{D+x(1+4*$b58xOh!C4ThK_C?urkT z?zJQIZqi17K0ZmDMqez;JaF|oc(6`0zookTaAdabtkB(4ZkSymj*#YNqs*3`h6Wjq zMDr(<+d!i52NC-w%>~zJo=ZQcdxwQz(loZ|g}mPZp?C^n_6#QZ`iY0<-h9ehtISG|_w+B-~2hA3N!cvvXIVo0T@Lz}$?B z`MBq8G+9mb)|=$6sHNhY!N}%ue0A5f08ZWRHxJ8IKCG+yZ47wZOT%qm&$tnXKK0kn zW@5IEz7GX_tT^(@c+-p!7dVEnXgY*!2!f!&#wC}z@G9(L`riA_9yH7O=w;Hm zm@iyR6rWENGtwt74jHUE$suimnb9r|#R<=wetK*Q9_5=uEe;8H5uke`at{(DWjoJI zb9GQF$i6d;UQX^)UXPyYCOkv!W%MC%zan~l5`PYP8vFG|+}tciKZ^Q<5uO~LNp499 zp3t77e1r1y9L0G5-oSaNiDUg{-LtZs$|rIe*-BTfoRODJy#o}}z(4l#=~dpJ4I+!~ zNmB9-o=nr)^s)3)yhHfUyR`(XtgFE1s*xzu5 z2JV(5a`s?Ig7S-kY8_1&Qec3h^zD6R#MeFWuNcx#5zO zU&LdWLH~2txPMT;>#<2_4P_@kFYGYKihjfcG()sjrJ$D~{U;L52tQxQr2?u0U$N+$ zfR1LOC}M{z){I+fNXGt6uTnjV{PZ2SBi<(&_yjIeO?eKbloXMu^@V!Ah$dB#_xE(I z0E;WKxF+>`@TMgF)y5~&u|QL(L-S3+C$>+z?jiY+F(3`G3cBa7Lg`QW@nfkERB~q? zf;*V+lEAY~OE-3fvyeYUO&SnWlHRh4wmFLPj75FYaLMM)KOm_O{8UX$@gd~X{2SK$ z%`JfUH->*e@1OX?I%B;+BtTgm*lK}}7)&pLV)SwZ^`PgTbrhAo!tu2$!6vKLWD4CL zVXy7v_SP;>@sanFZ}**Ea#<@PMZp$pKU9IdIGCSd6Q>HeuSXz}oNEz-3i_Rea}i`* zhL(Wvy)#CtGyAd&!t^nZ&eT(L4xZJo*&+l!LsdI=Csu2+-|2l&TeADtVIBba2mC+N ze|^4t$-AJ@EUZQiiqWjczj}DOa5{d*?+=pnX9L$ClaTz8md7}%8m5K6nM0#1Ul#i} z-Ypb|ABg>#4-(}$d~KoLPUZIdPtS%0zNh&=v8A57Sib!Qe~fJe1yx68|C%E$wEZ5W zm+EJHevtk8dxAa?u^BOTX)zc#RdNz9<;YE!)qsZvX96uu7DoGkm3RWdX{6 zvqYAb=vh=NR!zCcASz`%_{GQ`W@XGJTGFKB>e!i<;^TLs#9f<=oFz9j z3d%`XAhG&1RcQVv71Z2wj=S^M1EC6}mt^NLh^JQDpMGA9JUT`dZj4e)Cv_2)rcw`J z;61`}1}G0`^6^y(&86|gO(45^KZtE*L14-Fy-zflM++E7Aiz~96`}lof4NGo9E5#7 z2v=#8O9_mooWk0S%%%~{Jx>)eq)llNJIIP->%OTKYyEwGQ%2| zV%=N=RbQc&frrXz+Y6-fhsbv|HzjRI0u^4dU0_ypt6#cEHkmk*%0~d~AZOJ~_BkCN zj+*pY=^vy@gR~0}cjlPbPPOw61JCGm+aBJ6+N3hgXrA4w)bl5J^TArkC3jKvu1vFT z>B$m}Q*@wQi|9Amf=LHqX)KP8Qki%|46d^vkx6~vOhFj*T-Mg$HiJ&N0J!dDSe)Kp zf#5V@1h>DPg!JQA&lT}Jq^*kEdy#7bPt4NtgC+NEH29DXPT-!w$tFwY?JxVwo8~yA zd;|2D3Q9ztsF5&03HM~G@K1dxsBfRGXp`6*)%?mU%{GOdblEeR#?WgvPW}PDCv7Uc z^Oe&_p5)^?cCNWH;GGzum;$Mow$gt)gtQi|oi2Jmd+Uw}2-YF=2lNgHIY!A3xx+rg z-1R;kZB*>;#V?Zz+{bP;#4q997S5Djp+364@+lskDdJ@37xo7Jl5XpTri`7QR2amupiwXBKHW$>gP1h$|zy^0TEZ7?I6RcD@Xa*@T>_7KZ0CM3mr z++-Tvf1yq46cO^}wG3s9Oyjo|CLk6@4F;^;SM%bJhTV0lm}7Jj3iJ27NK_5t_bzwx zgv5Yk!+=}{SxuuIO^e`tU+x(~K6ch_{S*)*XY#&JiZs2S3DJl)Ff=eVc}}u6Twt;M zL1QaT%*jJ8L@VT=9+j)XQQ8%WYs%L-h9;>h(Xssu-)z?qqVE2_MONRszQ4qU?GaA>EUnEio+ag@*fn1;#MykU6M*JHN;{88xkFxI zDEQ5gruu@-T=# z*FdZ*CGitV-=oloKGpcM7zJM6tdzijScpr3?B)+42I%-{|*8y36Xf?_B z@5u7kn!j~)ck8~lE(cx=v;N$Exg@uBmygrEaJ<=EtJD#y)TMq9i-rJq4b9D+R!i%t*RwK>XI5E z)FE)_p~3JPMYFFp0akva)ue^qA-{bf{C;>Hdkr9))1yf@Qq9y8kw=@D%$k^_WRn*| z)iTHn=T{y=q`A-5>gpB=Yyx0p? zrIMO0sBGb!FLvhW+znBH_f2bK6yp&i#y7^&rqnLbvb@lTUXUU<(B^HM;+?_u{m(gU zSsl3asob`vLf_x>PRBx~F>J@Nc{rU799OT#?y7_#Q|uu!H35c24O}#PuKHA5#G`A% z;v$Y5dc+)S^1;J2O>p>z!3TBGhQrO%o}GwFa@__lPsEOV!E)@OSqa5!@{69 z&S*EB-bYWrRymrmHT(Gz4t zKJgm5up)BsK|~RBur|t>GIEDJxU*aWF>d{qu$VWBm05y?*VmY1uv*mjp>uBRw~e0d zoMD75|2v*jZuGZAK`-(P<(#yUT_`3QLod+_u7pj$&?39#n?5ktFlhz)TX-lP9K zQkQ+>M;U~xDcImZhU1bX`vdkrr7*k@z)bu9v?(RJ|62<4|5Lo;X6)+b-Qoq~rmER` z+_hSY4Qfr(2TWFbk--l*3ob_0ynEcPvV9v>dJ|mM)=yQ7`21Y1s?tnj3NEjwPYn;9 z4@l0-%d?nlw1d+ntT(5theymq{*c#dYS2Y z*>ZheKh5#oCAu(COl4LZ-;V#i-J4Q4q7t`Q*CWi|iw47L*<&p6tf&8XzVTL^-0F08 zl{A}U*My10n&Qn{pA+jXN%7}zTlZH|Z2_Pog}UiU95Y!VB4^^YI?N)v7J2>Khy{Ck ztVq5(Q-Ib`w?Leb^(`}R!Mg2iD(%e&?!H^aif-=og1*U3+|`Z3>^B(XHzhpR7#LM{ zjcBW&cC^qSp4>x%(6CgFHOGY<=5TKqc$K(epxvJU^4?ndEK@461^g;$+j+b{2|12b z6Kz>?ouqL0-x7w!?DT$MF?r_8x^7Za%OM9?%%j}QSRO-+@f|k4goZ8EkWNgvaG0d~ ziW{EIQdD!8(?fdt{HT`Naz6K4^?dPlHP&K-eQ1YU)}8K{nu$ZjA59o` zkh@;N@Pe{0mS&tb52v=L=Lv`GMGVFjnr;2%lWk`w-L+t1Zh^)&F;bYMcl!N?hM~i& z7C$f)!4S4gSk?Voz9dK9nO=n-tD&12x?PCe2CbOK$!&&%%}?H~%U53ncnRCpU{WU; z5AE^C_rf=sTqgncDVXwHg1aE(7BMpSdw!un2Z=Kv3AD6C2iaos*?B^}wZ}t1*s@{6b*27uGb*bKi znG!%cmnw-qGpfcjOk|~*W0;mxV~y9r`hr*0hi=9We~|z1>*&kpj7B z;ZFq2LJ)SKo)F?dwPEq(VPY9tgU^x1*H@4C4jx=Aj~VCrNz$2#K%`9Kqe{?QRQH&|kg=Oo$KVf8?z=&Ychm?~d8i>c@|G;Z%#D3E#7p^- zYAb=-%T%ChER1Yu!UUDSs++Uiv@u0BNbed0k6fQhMZ#dUBc~;{h=QmYgTdr^dH*9t z6Vzl3K&8bYfOID9S>B`|=bQ;2^`G{Q|76H}EQ0UF-em0G!|EtUQj$f12ix{Zs5Avag+N$p2kQd!;W#-f^tDm3= zs`D~`;9d^;0#AAST_t-5bj(QHVP$dwT~|P{uEbYCHwX!p0D8#7s&H;(G4XL*SK}mb zW?!O{OEA3@QY4PwTB53vCFlXfkod+vn)#Z;vBs27i6aR!ptP z;HKexM&62CDL?V`WawG68Z7jhZ#8C$)Rj*Tbo7i{j1-8a#i)*+0P@e70@P;CS%a`% z35+wA%3yvviYk@v0vfBotLLcCr{h-kgPRJ4$A`E_GA*zRq~20ww|l(7i#LyRkC$@@ zr-2MdSrDHKm}gkj)Pv^;8Fiv>xWGZnq6 z){$nHp*D*9kH8vd~-0TJ~vw9zR|^(g$v0kCG)v za@@2)h}Iz&9%6laj9U`~(DjXs@44O^w6TzFt9b7vm2+*I8xCZxffNrEm*V5;yu z^5aTYDlfe*5#O}Y}pbUu%%Zr_GZ1eNeejFGlM!;)9`BbaOsJ8(U;DSzD^{JROf$HnI z#9CXGou<9J0(!%y9{!=d1s_qKE6$JN7?37|$=XQ$1i8LPq_XA2+vq8B9xj@ooBQDi z(JA7>(=>E904j;h)C;!tj5rTo>FzqQgf$72n{Mcn8TQW%v*V{V%j%&)c1WgU*QB#`;Obim&IH1yZ_npx|q z+H5&ndo{y53#Ep9lZ0zmj>)YS|D$;WoyHAkb#pCSU16e?RZDA@ zanf1rY8O1Mb@SGog?*9o7IKTkxr4P@yxK`Z3?6%Vj6|?=6M0tKarJadhXjF}Y2zKk zbo{lIQWsp;ujctJbB5`4+P}*q{;UECO3`;@Fyi=#htBd6UGClrA#iW}-H@x!W6mOa z56MB;hMOHX%`_FE3~(E-K7JuBX}d(ZW0xD zY4@s7Xz%Cnr^Ta|Tj4*x>tCKgy_F&E2pZ!8a~=UId*&V1XIa8}!aFt~gsqj^G6uv4 zYww|KFVdcgL1%I6OCjqKadd$->rQd+EIUgd4sDd;L8E0(W zNu$Kka)AxD$9_)%hbuq%KFoZ$AKN!_aY_61G95d2j)a;*(NH)?A=Qx+j&cc3>Tv;} zyU)S?BLqYJIC4Mlo)}hlo&}eEe5MeP>pclz%xu%^DkhLnv(-xt0hT-k>c{>eh-I7z zDu6P|3c|H0IA&Ekj4{C7O`Z%k=oe4!z?mvy)n&_EJ&%EeiHWQX{!bbva$G_MKLK?U zihbVOh`~=okxNP=R{b*{{-{$<9Qen;jN++Kje>QjPI zP-r3)#!E#_gZj)9LfYWDgrmiE?(9Kk^Ou!CrK@?61W!4eLKOh%-ZcogJ(H=u45cb$ z2W*l1G>=B^EXEjcoI5{gNWI`U{~1DQ)@UwrheV2{5%;V%&|oc0m8cR!0!sBJOGTpU zIIhXK;T5uN9EwZ5m8_VL<%q!#($^(U2sX7>84tsabu${?o*JqTHGo4}lQt-%=azAM zhwUFi*d)%1X(tGRH*XrJ=%Yu&UZ}W)HGMYys3pz%4jsYxSFx-D@lZWOKl*vW^jW5c z8qK&-jkW{E@@3I<1}7_ud8k^Ko;@*E+{aFj^|?^ZT8wh~jFpN@6Ugn;NoN;pW9al_(e^#oz~OBXcu>GmR-^N~Y^DfGWT1&<_z|I3(X$z%&(#dEYcgB~qdG0& zm4Ui&JR>Ey%z;FFUBn;G0EjnB-iR)y^cY2dA3q8ncC>WcfNq!rG@HMphH0O319O~@ zuexCK!sg6Y>a-+;;AZ5Y?-`t?x}aZp19ee+1tLhh(eTTYjn9=y|m4U+9XNaIb$miJPYfpaohL#p})66Q+AO z^v0-%2}$vT3uw?GCB-o|B(kVWjlVE}1;4kbbEPBRtZqY~26FrkB*eIXHJ5~%j0_1o z5}~~3d)NlNr&_3dA%T7=dREPChQnz)nPwzRriR144Nz5y2rp-fY5(UA^-sqF5{&=7 zwA8-+BzEApu~vMcmW)t*dtXO(=@ZlR;YC5jFM!O2vCvx(p+@3P5h1tW-pz|vmNzyp zYMMoBY4TCjMOJ{A|1lw;h1}%SP&;kVk-AJYFVYEwRPoi z2~aw@J!1N73E;Jv!mrMD{nzqT%H^qsse# z9V!a<(~%tWHoK6|OMlU+@vET1uuHJIX`hs!qrh9Q#RSIm@1Ad_93Pj>-3%_L?*17b zs2s*>Cz;UXEl=wHe4AuOaKVUtbdr3&WIA=d|$gr^4x^V;WYRcRr^bb zOBFnZ;)AjZm&Y-0@83>XiC;Y+nw+5Gn@hX{s`5vVyVp7hp220NdeJ_VUX`&@Ln9OML#+t0#~1$z)({gzxA4$!oCqm2SEXoR;?YgO0=5WpUf2brA3wVKaT;x*`;5~`CGj3?f{X8se7oRZ@G$-d=J^l zu?Fsz{utX-T?G?_i+}FOXiSitfU%D5i-*TKIqix}(6=WX7zBfvFy|s4n1A!a`*VDY zicXG%{XH)4Daa@_Qb-Ue!lW(aCU@E_iA!2?9VO%oDY_LtWe1l^Xw>qgWy@mf2WtP# zk_@EPqj+)ZsN2?No#kS&wz?2Y57@6$x@p^)zZg!&Y zK;Itn%!tdjaG4VXZ{*>QUeE{U>AlPgk>grRum(?GtPIMRyX%AKhpMWNx!W;BLN|BV zQfL|R-!bbbh@Qc#gm#gyykd=M`8z4Un!@1RWeMU~4xQUC_Fl;Ga&gAZ*;E}9{zA7i z*<)yeFzmHVaF(ejiLzd)PA?fuH@8a1yOwz679#KUU$Cnp4b()6!tW6OssF|Qgf}0z zkqR@RVUUFC#m|3bRf~*x!otvZB(FAWUBdy&D(h5WflrhB z+_@FfvBFjBPQwKaZ~O2T`_6-PNBXepkFBE98RQN`9--Q^GRJLUQ`RYBm|%jYhFGju zyvpk4-U`E!TKXj5*a|7sy%{R6D`}Pqi(^R`YIIo6m?s#;cAL*t&2+mncFHK z5%GV_kxgDaU;Ep4?3c!%tyC4ZeETR>h;D_V%?0wbk&p5w!sL=j7zaq27Dc?sQ2NYSmV+Vg*K%^ayF1SFOI6len9n=c7jYo*(}pofeq^eZhs^xK zFDx0+X93+k4m1~^{W=tby7D>_iWCy%y4o}y&XmuGpalO2QjfQcgi8w%2VS(sp{Ci< zln;*3%lvgv&dh`|P;x|yBPUdd?}V>7ABrb;QFZ+MG2tS0aDa`&}E`=v-wAkF4_q(wT4r3LuTSNa``XdyyRk zohwwYv50Z~2!{@O;dK~F>BhJlSBP9oEQtFOh_FRDgJw#_Giy+loZc-m*9w)ba~^z0pj(*a>r8vs>64u9iy(@PO6m4aB=5*S2F5@sJ}Oxp!!3t3jFa zX0HlC_C53c8#gUh&7xjTOv@)FYJyWVsLJIPk3vOxTIsaMP8Y(x05!4Y7dQbQ@wG`dZwESxVlEC=Lyi(XWD%~n(QjO8Qc7Hgn-@nj%Ht@uy z)T}9)N1mYXRb9n?Tw`1!H|}+%6BH{{hzQL&oyIwa3xa~YGctH{$ykB8B6~xEpl8-^ zx@EeLaZ>QSz78`h$8h*f%6rJ(FeC7yNFm%#-P6`~2o$S#6g?lyt2y7?(tX33KGgKw zo!SXW4Jsp&Xxn&X^Y|cIB(}&5q4DA7A84Bm&l;=BD+3y4F*I@l&&m?+wQ(i%S#B`Y z$_D5Qmw1Taof_T7KS9s&F~-p78{u0!jN?7`^5FL@9_(JJ9-=eX;nrx7LY3l~NnJdT zpiX!saOE{QzfDPfP~4BevtD8}*aE zLKWa-R9OHB^SCy|nfboCMC}+t?-3jIhfoTDq^m6KzAg)a4JRuytPZt*>HsG2^kU0} z=!KV{^KRlnoD#b}MQ=ZO+o@9wfZvVb<&ztB2*I9+88?vdr$4wNed=8^V~iRBlJ5Hs z2<6lI4MCE?r)q*K@=YD^hbI`<1q#B5d`Ej`COE}aSY+Nu*-{clRH8EbS*-m6(=R_% zh=P&dVr_^3agH^PAUWD@%g4F-x@!~UKK&feDly=kw8NZh6K2uwp3$j0fEX@IBr6Ek2Ok%-=!fhTqX%czBzhZ zD^zT6wGv+W@SpK1Cel`oSh8oZ@eZ&{GbW6S61gto|JEQP$3{nUw<;JzE4Ed`5^y_bHPw-qz~Kqh7j#z9lh$dh3Px(GoKh zE`lGbFF6wuoPZp9^uM+@X~*P3h4RVZch{1m0OFt!hRIp>UOzPHz+1o0*qDfFP_y*@ zo&F{JOB0}aw+gOz$}-puZJejaf2$?QMI`OCb0LjaAzyZsWXi!|>aQHpBMzTK5Sj~r zd7L@gd8AC6`|~MBc}SVI6bWxP^lO+v&&D+I$5kZZb0R9BC)ttA~oGwC69T<>GF3cO!1T!+?Bnw zV(2Ut6kocY>TR_w8=3oaV}yV5#qR`njBo5HHX##PU%itkeW*A}Uz@K2P3lDCBXzTC zc@B@Qe?PukEe=QDZ;Hxq^ZE+F7XRNnWi0ha>3`*Og$7{!;eqJ#RlWJa+21a4Vpj>5 zH7a)+K=gQz{s!YV7rR$muPjVYYb`QwXaRbg<27Zh3I@sz8n#r7-@-n2;J_J(lIu@o z6a&8yQ&!u*_z`v!%G;RE@m^|3WpThwr#{zQ^jAIZ%5)Jqv3~e~N!n0~*=^|8;P-ut ze@p4rh^kQ7m5LiMp2?j9WzPp-I|XRhdMNR+!gon4N$yG(<-CX%&g#46kWk+1=SG-> z5=mrAerWJX>sff5G^9&qW0dNXQ{{2&=sA-mNyOk0ox_eaFlbVtoqeo9>6fQ#F+FPO zQn?WQvOEAMpue%|xJKbN=I1xi;PQV|a@J8%c3T4_o6_mP8fu_o=_QYb4Tq3Y zW~jc`S3ufci6ZLA2n_6=z~#?xdWp2E6W+}!sBSDboxxWatJ~n97MYPYSrD>=Ydnme z(viw_&1UM$zhMxY;1)05PkdE-SVAT#dygcQUSArVM^~U+-EU~W*-p{zUpSSyL$KaOzsvX+qQIb8I$*csU~GQW z!XPfmfG9V}2!tt!;x#byXx+6d%)1DWQ~#3xDOoawQXYWU04gtgy)5QZ!V6W&#PYfV zG~gVGc7-%qF$_f2@G2NH8q3l^<6}c+0}i)kK%(e*>|8AIYW+Qx{IYh%FE-3)O)EFG zQ$qI&qn>)c67pnyxI8?)Qnq#>x9RJ=%0h5Fs6fQIOj{i*Wtd8a$-*|Z{VTk*U&aaf^E-<3YZ#vZK|l%>iAw)99j(z`sU zw5ITc?F8nw9vgl5WCC7xY(S_EX!P~AKNpavugYZpd?G>JjzS8wC34YN!Ob972alM(I0%2?xq-ZrHeq6%Ct!~7LmaX;3 zyYQTD{6?~uh+<(C5F^Iv`B+J-iX$}N>_~?D%^P13b~y|HM8D2C z@@llPiO^<{vw&t}8V!BA<}lAC`qsdDljxXK=g~O|Bl()%1yxBZ06;-L{~zRpoTUTQ(v|Mt{D^jKLk$8Q z&~pgB?$hAjKy4{@yJ^%X+97s|#TA&yHArmY4)?385y}Fse)C3WxTizLtIC}9 znatAGLcWVD(>tf4t=Om-yIQG(Syfy9V^{v?S1ZW@NT6;Npi4*FNCT$nY{)D1P7uDL zU#LApBXRxCNy2+WG_^3NfVt|11Srtyp^7(x5K9jP`vi;<-m8P*(T_v>W?IIS$Dr@1 z8d$tY8knmT%zR5tnnQ*BdxLwFTR!7Sx;{|~8iINYB#EbHP_`SlN!IoB$rRK9!)4s3 zs)1+3l6;w|<~9_^$u8vL64I#hMBH>Aw>#IsdX$zC%~3L$BgP(bo1|*I%^U+m?uAWr ze(P`Xwzs{L>@`EdwmKpj4#M614trVVRTZp))XWb-Ck$0{b*Ya^2IPs_1^eC?F80zc z^+81_Qo23nJnI)vglRw3r6NnV;-=TK((vQ;lb*^8k0k`bQW%Q_;O2sFXgx&X>q(BI z&$R6V_Zw_ta~dP(QF;tVu%|TRtW-tIXvp1`s)<1@0~9#O_=z!jRLPna(9|5a@tokY z4R!wsE>>7|M4Cv(@~GNzIhv`y-ec5F$KlN|dUBeMupm#Hrm;9*Doiv^&whxFyk$s%f=oS1=WGM4kT$`p4FS)~rjmlEUSgD7cgvGwE%)i8^EGU8o7N0Pw@-tXf&Y+M|p;b$4jt%X`y+H5zo zE^I_Ug7DPe7Z6t9m`i5#VK?Zm)N@Bj20;w6_g9;leBu(Fod&*ymD?4#e4pId z6c`Zc8`zv1P%^wg(Frpbtx*Q@III(dttIKIDBddiA?XE>cG)LB@A<&c0b5lOdHx-! za7vo19?ztWl6NBx>5|C0c$rxkD8CihO+p=Nan!;rWG#;%FR5Q|Z3?xT>r^X*o=qTd zw(IA_IrY$xPYfTffUQNi!f&wkLMGzSCy}sh_3mX|b%JL7LW}VShg~i!uWrq@xytIRo`ra&(mFK=mr$ufl(+h&*Ds@jX3(EH8)#)^| zq^&Mr_$IwJM})o!kqdHBWuC_pOGK;bl00B}Y?>#~N2a*&t0szWI!j}n8hmytrZ+1{ zla$pBIDTNp;zO)NKXe4t)wfjBY?Sb0U7eGckvm9y1ZTIT&x))ykq!}hOQJuE3XiI( z&+@;wU`$ZPm*t-U*_1M^W9!h8-D|$j73CwNCO0!yy|#aVEWna5@0x1a6^C%lH%E!a z7dHeu5SHn71_$g4=iwPkDx8MLCK)Q>6} zL@Q3iaw0;S=q~>KF=}#DAKwgB!t4_v3`peAkm6)c&uZmdNZVdiuNPbN6T#YZZozTx zU?zttRGYtI;7l7PWtL(0$iJHQ`y3IZsrC)|QEc@rdhwM9JO^gj;PmSZmgCLFQUy)Z zM*bx;t6zXQQ@*0HlKft?&4Gm&4Zen|UHJ7?QTJu9tz2Sc8oA}1n*}qb44xGHDdi@) zAo-3IF(_;H?Pc2R_-nK>CHNPLe8!&nW-))1MihRtXdEVP8HNrdHFXqjwP+S3(s zH3TTd;yMA@)+)8ee)#0Uj^FA+fApK{U}22GNqX+ct=tT~cbD10h7cfGMAug;x~KHi z+n%FadDKHQf&To|dAZfKSh^aIPIwb}B;X*FB8699A}*xxa?_Ml86v?^SX=f)(3gF> z(c6SlDzum5s^k;b)D-#&58P?IFTfv373*iNsh7A5EczD4;AKGF>a%Xg{-iXP`gf|A zqgKphb|sEWG+V*#EeGYZ3laz|->61Cdt8VRW=X5W9>%7^3q~EnZLS4#L_5tav{+?- z?otv#xbHC{!G(SMKIg)4m>kaqDzlF}R?umKs)^y$ucSYfZG~=4fSJRO;h!rHn@;5A zXxAb1y5B-OS_ zatO!AQ+&C2XIdSa73s?LkWgOTMz!&NC{k^$biTzEXpI_QPU}VI zYXMuHxM$PGuQLlE{Cq$SFDvV3#k`_AW?~W);}C;OLXlP-OWVvojkX=GpMAmZ zWEKi3%uup`7EG0Zk)@9MPqffWMzUT^^yrpJWGl>zD)QJao8@`@U{pMQuEqmLmEE+| z)=jUT!`2zjXK^I}vMuix-E{GH>9x?@fznfz1p*>;crdU=>idxZ^A6~_UevkMRH*X>rTjKn+w<36e8*Z-#o9bNu4auRy!F*xB-K3$fdhvbxu~{+ygw!D+69Rs1XukvO?l^mY%0E%} zR27ksu>k)JyZdFO>qql%=(~Sby@TKVlKmge|Df>x?DOxH#&Ww~|FHLS&;Pr<-@}Ky6%{@o} literal 0 HcmV?d00001 diff --git a/project/MOOCSettings.scala b/project/MOOCSettings.scala new file mode 100644 index 0000000..171244f --- /dev/null +++ b/project/MOOCSettings.scala @@ -0,0 +1,46 @@ +package ch.epfl.lamp + +import sbt._ +import sbt.Keys._ + +/** + * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have + * different item ids. + * + * @param key Assignment key + * @param partId Assignment partId + * @param itemId Item id of the non premium version + * @param premiumItemId Item id of the premium version (`None` if the assignment is optional) + */ +case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String]) + +/** + * Settings shared by all assignments, reused in various tasks. + */ +object MOOCSettings extends AutoPlugin { + + object autoImport { + val course = SettingKey[String]("course") + val assignment = SettingKey[String]("assignment") + val options = SettingKey[Map[String, Map[String, String]]]("options") + val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment") + val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment") + // Convenient alias + type CourseraId = ch.epfl.lamp.CourseraId + val CourseraId = ch.epfl.lamp.CourseraId + } + + import autoImport._ + + override val globalSettings: Seq[Def.Setting[_]] = Seq( + // supershell is verbose, buggy and useless. + useSuperShell := false + ) + + override val projectSettings: Seq[Def.Setting[_]] = Seq( + parallelExecution in Test := false, + // Report test result after each test instead of waiting for every test to finish + logBuffered in Test := false, + name := s"${course.value}-${assignment.value}" + ) +} diff --git a/project/StudentTasks.scala b/project/StudentTasks.scala new file mode 100644 index 0000000..7604830 --- /dev/null +++ b/project/StudentTasks.scala @@ -0,0 +1,318 @@ +package ch.epfl.lamp + +import sbt._ +import Keys._ + +// import scalaj.http._ +import java.io.{File, FileInputStream, IOException} +import org.apache.commons.codec.binary.Base64 +// import play.api.libs.json.{Json, JsObject, JsPath} +import scala.util.{Failure, Success, Try} + +/** + * Provides tasks for submitting the assignment + */ +object StudentTasks extends AutoPlugin { + + override def requires = super.requires && MOOCSettings + + object autoImport { + val packageSourcesOnly = TaskKey[File]("packageSourcesOnly", "Package the sources of the project") + val packageBinWithoutResources = TaskKey[File]("packageBinWithoutResources", "Like packageBin, but without the resources") + val packageSubmissionZip = TaskKey[File]("packageSubmissionZip") + val packageSubmission = inputKey[Unit]("package solution as an archive file") + val runGradingTests = taskKey[Unit]("run black-box tests used for final grading") + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + // submitSetting, + runGradingTestsSettings, + + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ packageSubmissionZipSettings + + lazy val runGradingTestsSettings = runGradingTests := { + val testSuiteJar = "grading-tests.jar" + if (!new File(testSuiteJar).exists) { + throw new MessageOnlyException(s"Could not find tests JarFile: $testSuiteJar") + } + + val classPath = s"${(Test / dependencyClasspath).value.map(_.data).mkString(File.pathSeparator)}${File.pathSeparator}$testSuiteJar" + val junitProcess = + Fork.java.fork( + ForkOptions(), + "-cp" :: classPath :: + "org.junit.runner.JUnitCore" :: + (Test / testSuite).value :: + Nil + ) + + // Wait for tests to complete. + junitProcess.exitValue() + } + + + /** ********************************************************** + * SUBMITTING A SOLUTION TO COURSERA + */ + + val packageSubmissionZipSettings = Seq( + packageSubmissionZip := { + val submission = crossTarget.value / "submission.zip" + val sources = (packageSourcesOnly in Compile).value + val binaries = (packageBinWithoutResources in Compile).value + IO.zip(Seq(sources -> "sources.zip", binaries -> "binaries.jar"), submission) + submission + }, + artifactClassifier in packageSourcesOnly := Some("sources"), + artifact in (Compile, packageBinWithoutResources) ~= (art => art.withName(art.name + "-without-resources")) + ) ++ + inConfig(Compile)( + Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++ + Defaults.packageTaskSettings(packageBinWithoutResources, Def.task { + val relativePaths = + (unmanagedResources in Compile).value.flatMap(Path.relativeTo((unmanagedResourceDirectories in Compile).value)(_)) + (mappings in (Compile, packageBin)).value.filterNot { case (_, path) => relativePaths.contains(path) } + }) + ) + + val maxSubmitFileSize = { + val mb = 1024 * 1024 + 10 * mb + } + + /** Check that the jar exists, isn't empty, isn't crazy big, and can be read + * If so, encode jar as base64 so we can send it to Coursera + */ + def prepareJar(jar: File, s: TaskStreams): String = { + val errPrefix = "Error submitting assignment jar: " + val fileLength = jar.length() + if (!jar.exists()) { + s.log.error(errPrefix + "jar archive does not exist\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength == 0L) { + s.log.error(errPrefix + "jar archive is empty\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength > maxSubmitFileSize) { + s.log.error(errPrefix + "jar archive is too big. Allowed size: " + + maxSubmitFileSize + " bytes, found " + fileLength + " bytes.\n" + + jar.getAbsolutePath) + failSubmit() + } else { + val bytes = new Array[Byte](fileLength.toInt) + val sizeRead = try { + val is = new FileInputStream(jar) + val read = is.read(bytes) + is.close() + read + } catch { + case ex: IOException => + s.log.error(errPrefix + "failed to read sources jar archive\n" + ex.toString) + failSubmit() + } + if (sizeRead != bytes.length) { + s.log.error(errPrefix + "failed to read the sources jar archive, size read: " + sizeRead) + failSubmit() + } else encodeBase64(bytes) + } + } + + /** Task to package solution to a given file path */ + lazy val packageSubmissionSetting = packageSubmission := { + val args: Seq[String] = Def.spaceDelimited("[path]").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val base64Jar = prepareJar(jar, s) + + val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath) + scala.tools.nsc.io.File(path).writeAll(base64Jar) + } + +/* + /** Task to submit a solution to coursera */ + val submit = inputKey[Unit]("submit solution to Coursera") + lazy val submitSetting = submit := { + // Fail if scalafix linting does not pass. + scalafixLinting.value + + val args: Seq[String] = Def.spaceDelimited("").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val assignmentDetails = + courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined")) + val assignmentKey = assignmentDetails.key + val courseName = + course.value match { + case "capstone" => "scala-capstone" + case "bigdata" => "scala-spark-big-data" + case other => other + } + + val partId = assignmentDetails.partId + val itemId = assignmentDetails.itemId + val premiumItemId = assignmentDetails.premiumItemId + + val (email, secret) = args match { + case email :: secret :: Nil => + (email, secret) + case _ => + val inputErr = + s"""|Invalid input to `submit`. The required syntax for `submit` is: + |submit + | + |The submit token is NOT YOUR LOGIN PASSWORD. + |It can be obtained from the assignment page: + |https://www.coursera.org/learn/$courseName/programming/$itemId + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + """.stripMargin + s.log.error(inputErr) + failSubmit() + } + + val base64Jar = prepareJar(jar, s) + val json = + s"""|{ + | "assignmentKey":"$assignmentKey", + | "submitterEmail":"$email", + | "secret":"$secret", + | "parts":{ + | "$partId":{ + | "output":"$base64Jar" + | } + | } + |}""".stripMargin + + def postSubmission[T](data: String): Try[HttpResponse[String]] = { + val http = Http("https://www.coursera.org/api/onDemandProgrammingScriptSubmissions.v1") + val hs = List( + ("Cache-Control", "no-cache"), + ("Content-Type", "application/json") + ) + s.log.info("Connecting to Coursera...") + val response = Try(http.postData(data) + .headers(hs) + .option(HttpOptions.connTimeout(10000)) // scalaj default timeout is only 100ms, changing that to 10s + .asString) // kick off HTTP POST + response + } + + val connectMsg = + s"""|Attempting to submit "${assignment.value}" assignment in "$courseName" course + |Using: + |- email: $email + |- submit token: $secret""".stripMargin + s.log.info(connectMsg) + + def reportCourseraResponse(response: HttpResponse[String]): Unit = { + val code = response.code + val respBody = response.body + + /* Sample JSON response from Coursera + { + "message": "Invalid email or token.", + "details": { + "learnerMessage": "Invalid email or token." + } + } + */ + + // Success, Coursera responds with 2xx HTTP status code + if (response.is2xx) { + val successfulSubmitMsg = + s"""|Successfully connected to Coursera. (Status $code) + | + |Assignment submitted successfully! + | + |You can see how you scored by going to: + |https://www.coursera.org/learn/$courseName/programming/$itemId/ + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + |and clicking on "My Submission".""".stripMargin + s.log.info(successfulSubmitMsg) + } + + // Failure, Coursera responds with 4xx HTTP status code (client-side failure) + else if (response.is4xx) { + val result = Try(Json.parse(respBody)).toOption + val learnerMsg = result match { + case Some(resp: JsObject) => + (JsPath \ "details" \ "learnerMessage").read[String].reads(resp).get + case Some(x) => // shouldn't happen + "Could not parse Coursera's response:\n" + x + case None => + "Could not parse Coursera's response:\n" + respBody + } + val failedSubmitMsg = + s"""|Submission failed. + |There was something wrong while attempting to submit. + |Coursera says: + |$learnerMsg (Status $code)""".stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera responds with 5xx HTTP status code (server-side failure) + else if (response.is5xx) { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera seems to be unavailable at the moment (Status $code) + |Check https://status.coursera.org/ and try again in a few minutes. + """.stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera repsonds with an unexpected status code + else { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera replied with an unexpected code (Status $code) + """.stripMargin + s.log.error(failedSubmitMsg) + } + } + + // kick it all off, actually make request + postSubmission(json) match { + case Success(resp) => reportCourseraResponse(resp) + case Failure(e) => + val failedConnectMsg = + s"""|Connection to Coursera failed. + |There was something wrong while attempting to connect to Coursera. + |Check your internet connection. + |${e.toString}""".stripMargin + s.log.error(failedConnectMsg) + } + + } +*/ + + def failSubmit(): Nothing = { + sys.error("Submission failed") + } + + /** + * ***************** + * DEALING WITH JARS + */ + def encodeBase64(bytes: Array[Byte]): String = + new String(Base64.encodeBase64(bytes)) +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..a919a9b --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.8 diff --git a/project/buildSettings.sbt b/project/buildSettings.sbt new file mode 100644 index 0000000..8fac702 --- /dev/null +++ b/project/buildSettings.sbt @@ -0,0 +1,5 @@ +// Used for Coursera submission (StudentPlugin) +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4" +// Used for Base64 (StudentPlugin) +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..017735d --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.0") diff --git a/src/main/scala/actorbintree/BinaryTreeSet.scala b/src/main/scala/actorbintree/BinaryTreeSet.scala new file mode 100644 index 0000000..e1c210d --- /dev/null +++ b/src/main/scala/actorbintree/BinaryTreeSet.scala @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package actorbintree + +import akka.actor._ +import scala.collection.immutable.Queue + +object BinaryTreeSet { + + trait Operation { + def requester: ActorRef + def id: Int + def elem: Int + } + + trait OperationReply { + def id: Int + } + + /** Request with identifier `id` to insert an element `elem` into the tree. + * The actor at reference `requester` should be notified when this operation + * is completed. + */ + case class Insert(requester: ActorRef, id: Int, elem: Int) extends Operation + + /** Request with identifier `id` to check whether an element `elem` is present + * in the tree. The actor at reference `requester` should be notified when + * this operation is completed. + */ + case class Contains(requester: ActorRef, id: Int, elem: Int) extends Operation + + /** Request with identifier `id` to remove the element `elem` from the tree. + * The actor at reference `requester` should be notified when this operation + * is completed. + */ + case class Remove(requester: ActorRef, id: Int, elem: Int) extends Operation + + /** Request to perform garbage collection */ + case object GC + + /** Holds the answer to the Contains request with identifier `id`. + * `result` is true if and only if the element is present in the tree. + */ + case class ContainsResult(id: Int, result: Boolean) extends OperationReply + + /** Message to signal successful completion of an insert or remove operation. */ + case class OperationFinished(id: Int) extends OperationReply + +} + + +class BinaryTreeSet extends Actor { + import BinaryTreeSet._ + import BinaryTreeNode._ + + def createRoot: ActorRef = context.actorOf(BinaryTreeNode.props(0, initiallyRemoved = true)) + + var root = createRoot + + // optional (used to stash incoming operations during garbage collection) + var pendingQueue = Queue.empty[Operation] + + // optional + def receive = normal + + // optional + /** Accepts `Operation` and `GC` messages. */ + val normal: Receive = { case _ => ??? } + + // optional + /** Handles messages while garbage collection is performed. + * `newRoot` is the root of the new binary tree where we want to copy + * all non-removed elements into. + */ + def garbageCollecting(newRoot: ActorRef): Receive = ??? + +} + +object BinaryTreeNode { + trait Position + + case object Left extends Position + case object Right extends Position + + case class CopyTo(treeNode: ActorRef) + /** + * Acknowledges that a copy has been completed. This message should be sent + * from a node to its parent, when this node and all its children nodes have + * finished being copied. + */ + case object CopyFinished + + def props(elem: Int, initiallyRemoved: Boolean) = Props(classOf[BinaryTreeNode], elem, initiallyRemoved) +} + +class BinaryTreeNode(val elem: Int, initiallyRemoved: Boolean) extends Actor { + import BinaryTreeNode._ + import BinaryTreeSet._ + + var subtrees = Map[Position, ActorRef]() + var removed = initiallyRemoved + + // optional + def receive = normal + + // optional + /** Handles `Operation` messages and `CopyTo` requests. */ + val normal: Receive = { case _ => ??? } + + // optional + /** `expected` is the set of ActorRefs whose replies we are waiting for, + * `insertConfirmed` tracks whether the copy of this node to the new tree has been confirmed. + */ + def copying(expected: Set[ActorRef], insertConfirmed: Boolean): Receive = ??? + + +} diff --git a/src/test/scala/actorbintree/BinaryTreeSuite.scala b/src/test/scala/actorbintree/BinaryTreeSuite.scala new file mode 100644 index 0000000..46ecca4 --- /dev/null +++ b/src/test/scala/actorbintree/BinaryTreeSuite.scala @@ -0,0 +1,126 @@ +/** + * Copyright (C) 2009-2015 Typesafe Inc. + */ +package actorbintree + +import akka.actor.{ActorRef, ActorSystem, Props, actorRef2Scala, scala2ActorRef} +import akka.testkit.{ImplicitSender, TestKit, TestProbe} +import org.junit.Test +import org.junit.Assert._ + +import scala.util.Random +import scala.concurrent.duration._ + +class BinaryTreeSuite extends TestKit(ActorSystem("BinaryTreeSuite")) with ImplicitSender { + + import actorbintree.BinaryTreeSet._ + + def receiveN(requester: TestProbe, ops: Seq[Operation], expectedReplies: Seq[OperationReply]): Unit = + requester.within(5.seconds) { + val repliesUnsorted = for (i <- 1 to ops.size) yield try { + requester.expectMsgType[OperationReply] + } catch { + case ex: Throwable if ops.size > 10 => sys.error(s"failure to receive confirmation $i/${ops.size}\n$ex") + case ex: Throwable => sys.error(s"failure to receive confirmation $i/${ops.size}\nRequests:" + ops.mkString("\n ", "\n ", "") + s"\n$ex") + } + val replies = repliesUnsorted.sortBy(_.id) + if (replies != expectedReplies) { + val pairs = (replies zip expectedReplies).zipWithIndex filter (x => x._1._1 != x._1._2) + fail("unexpected replies:" + pairs.map(x => s"at index ${x._2}: got ${x._1._1}, expected ${x._1._2}").mkString("\n ", "\n ", "")) + } + } + + def verify(probe: TestProbe, ops: Seq[Operation], expected: Seq[OperationReply]): Unit = { + val topNode = system.actorOf(Props[BinaryTreeSet]) + + ops foreach { op => + topNode ! op + } + + receiveN(probe, ops, expected) + // the grader also verifies that enough actors are created + } + + @Test def `proper inserts and lookups (5pts)`(): Unit = { + val topNode = system.actorOf(Props[BinaryTreeSet]) + + topNode ! Contains(testActor, id = 1, 1) + expectMsg(ContainsResult(1, false)) + + topNode ! Insert(testActor, id = 2, 1) + topNode ! Contains(testActor, id = 3, 1) + + expectMsg(OperationFinished(2)) + expectMsg(ContainsResult(3, true)) + () + } + + @Test def `instruction example (5pts)`(): Unit = { + val requester = TestProbe() + val requesterRef = requester.ref + val ops = List( + Insert(requesterRef, id=100, 1), + Contains(requesterRef, id=50, 2), + Remove(requesterRef, id=10, 1), + Insert(requesterRef, id=20, 2), + Contains(requesterRef, id=80, 1), + Contains(requesterRef, id=70, 2) + ) + + val expectedReplies = List( + OperationFinished(id=10), + OperationFinished(id=20), + ContainsResult(id=50, false), + ContainsResult(id=70, true), + ContainsResult(id=80, false), + OperationFinished(id=100) + ) + + verify(requester, ops, expectedReplies) + } + + + @Test def `behave identically to built-in set (includes GC) (40pts)`(): Unit = { + val rnd = new Random() + def randomOperations(requester: ActorRef, count: Int): Seq[Operation] = { + def randomElement: Int = rnd.nextInt(100) + def randomOperation(requester: ActorRef, id: Int): Operation = rnd.nextInt(4) match { + case 0 => Insert(requester, id, randomElement) + case 1 => Insert(requester, id, randomElement) + case 2 => Contains(requester, id, randomElement) + case 3 => Remove(requester, id, randomElement) + } + + for (seq <- 0 until count) yield randomOperation(requester, seq) + } + + def referenceReplies(operations: Seq[Operation]): Seq[OperationReply] = { + var referenceSet = Set.empty[Int] + def replyFor(op: Operation): OperationReply = op match { + case Insert(_, seq, elem) => + referenceSet = referenceSet + elem + OperationFinished(seq) + case Remove(_, seq, elem) => + referenceSet = referenceSet - elem + OperationFinished(seq) + case Contains(_, seq, elem) => + ContainsResult(seq, referenceSet(elem)) + } + + for (op <- operations) yield replyFor(op) + } + + val requester = TestProbe() + val topNode = system.actorOf(Props[BinaryTreeSet]) + val count = 1000 + + val ops = randomOperations(requester.ref, count) + val expectedReplies = referenceReplies(ops) + + ops foreach { op => + topNode ! op + if (rnd.nextDouble() < 0.1) topNode ! GC + } + receiveN(requester, ops, expectedReplies) + } +}