Sbt #2: Proje yonetimi

Postun basligi iki yillik bolum adi gibi oldu ama idare edelim. 

IntelliJ Yeni projesi
Oncelikle IntelliJ ile bir sbt projesi olsuturuyoruz. (IntelliJ community edition ucretsiz ve scala plugini de kurulabiliyor). Sbt'nin de sisteminizde yuklu oldugunu varsayarak (onceki postta bahsetmistik), yeni proje'ye tikliyoruz ve sbt'yi seciyoruz. Projeye bir isim verim tamam'a tikliyoruz. Akabinde IntelliJ hemen build.sbt'yi yuklemeye baslayacak ve karsimiza su sekilde bir klasor yapisi cikacaktir:

build.sbt'ye goz atarsak, 3 adet key tanimlanmis oldugunu goruyoruz. Ek olarak IntelliJ spesifik dosyalari ve de projeyi build ettikten sonra olusan artifactlari source repository'e gondermemek icin bir .gitignore dosyasi olusutup su iki satiri ekliyoruz:

.idea/
target/

Sub-project yapisi
Genisletilebilir (extensible) bir proje yapisi elde etmek icin proje icerisinde sub-proje'ler olsuturmak yaygin bir yaklasim. Bu sayede buyuk bir projenin alt sistemlerini izole veya birbirine depend eden kisimlari belirleyerek maintain edebiliyoruz. Simdi bir ana proje (root) ve bir de sub-project olmak uzere iki proje tanimlamasi yapalim:

val root = project.in(file("."))
val sayisalLoto = project

burada project keywordu ile iki adet sub-project tanimlamasi yaptik. Ilki root isimli ve ana projeyi temsil ediyor. Bunu da root klasor uzerinde tanimlanmasi icin file(".") seklinde yaptik. Diger sub-project ise sayisalLoto adinda, klasor tanimlamasi yapmadigimiz icin, build.sbt reload edildiginde sayisalLoto isimli bir klasor olusturacak. build.sbt'yi reload edip yaptigimiz degisikliklerin gecerli olmasi icin IntelliJ'in sbt plugini uzerinde reload'a tiklayabilir ya da komut satirindan sbt reload calistirabiliriz. 

Reload ettikten sonra, sayisalLoto isimli bir klasor olusturulacaktir. Hemen ilk dosyasimizi olusturmak icin bu klasore sag tiklayip, new/file seceneginden soyle bir path giriyoruz:

src/main/scala/SayisalTahmin.scala

Sayisal loto tahmin programimizi impente edelim:

object SayisalTahmin extends App {
       println((0 to 5).map(_ => scala.util.Random.nextInt(50)).mkString(","))
}

Bu sayede IntelliJ, ayni zamanda ara klasorleri de src/main/scala olsuturacaktir. Simdi InteeliJ icerisinden terminal acarak projemizi derleyelim:

> sbt
> ;reload ;compile

Ama o da ne? Projede bir degisiklik yapilmadigi goruluyor ama biz bir dosya ekledik oysa ki? Bunun sebebi suan iki farkli sub-project olmasi ve root'un aktif projec olarak secili olmasidir. Bunu projects komutu ile dogrulayabiliriz:

sbt:hello_sbt> projects
[info] In file:/hello_sbt/
[info]   * root
[info]     sayisalLoto

Peki sayisalTahmin sub-projesini nasil derleyebiliriz? Bunu yapmanin 3 farkli yolu var:

1. sayisalLoto/compile seklinde proje/komut calistirmak. 

2. Derlemek veya uzerinde calismak istedigimiz projeyi aktif proje olarak secmek. project sayisalLoto komutunu calistirip bunu yapabiliriz. Daha sonra normal olarak ;reload ;compile gibi calismaya devam edebiliriz. Ayrica bunu yaptigimizda sbt promt'un da degistigini goruyoruz. Gorsel olarak hangi proje uzerinde calistigimizi gormek gayet yararli. 

sbt:sayisalLoto> clean
[success] Total time: 0 s, completed 09-Jan-2021 08:25:42
sbt:sayisalLoto> 

3.  Son yontem ise projeler arasinda bir iliski tanimlamak. root sub-projesini, sayisalLoto sub-projesinin aggregator'u olarak tanimlayabiliriz. Hatta baska sub-projelerimiz var ise onlari da dahil edebiliriz. Bu durumda root uzerinde calsitirilan bir komut (clean veya compile gibi) tum aggregated sub-projelere iletilir ve onlar uzerinde de calistirilir. Ozellikle bircok sub-project barindiran buyuk projelerde bu yontem bize cok zaman kazandiracktir. Cunku root uzerinde ornegin compile calistirdigimizda tum projeyi tek komutla derlemis olacagiz. Bunu yapmak icin build.sbt dosyasinda soyle degisiklik yapiyoruz:

val root = project.in(file("."))
                            .aggregate(sayisalLoto)

val sayisalLoto = project

Simdi bu degisiklikleri yapip reload edersek tertemiz bir NPE ile karsilasiyoruz

Caused by: java.lang.NullPointerException

Bunun sebebi ise cok basit. Aggregate metodu icerisinde henuz daha deger atamasi yapilmamis olan sayisalLoto degiskenine atifta bulunduk. sayisalLoto tanimlamasini, root tanimlamasinin uzerine alabiliriz ama bu cok da elegant bir cozum olmaz. Bunu cozmek icin iki proje tanimlamasini da lazy olarak degistiriyoruz ve cok elegant bir sekilde problem cozuluyor. 

lazy val root = project(file(".")).aggregate(sayisalLoto)
lazy val sayisalLoto = project

Daha sonra root projede iken ;clean ;compile yaparsak var olan scala dosyamizin da derlendigini goruyoruz ki demek ki aggregation calisiyor. Bu son yontem buyuk projelerin ship edilmesinde fayda saglarken diger iki yontem development esnasinda belirli sub-proje'ye odaklanmayi saglar. 

Aslinda eger sub-projeleiniz varsa ve bir aggregate projeniz yoksa sbt bir tane olusturacaktir. Bunu ispatlamak icin build.sbt'ye giderek root tanimlamasini comment'leyip projede reload calistiriyoruz. Tamamen ayni davranisi elde ettik. projects komutu ile projelere bakarsak bu sefer root yerinse hello_sbt isimli ana projenin (build.sbt'deki proje adi) olusturuldugunu goruyoruz. Ana projede iken sayisalTahmin classimizi calistirmaya calisalim. 

sbt:hello_sbt> ;clean ;run
[success] Total time: 0 s, completed 10-Jan-2021 06:32:10
[error] java.lang.RuntimeException: No main class detected.

Beklenen birseyle karsilasiyoruz cunku ana proje icerisinde tanimli bir class yok ve run komutu sadece spesifik bir proje icerisindeki class'lari calistirabilir. Bu durumda sub-proje adi da belirtmemiz gerekiyor:

sbt:hello_sbt> sayisalLoto/run
[info] compiling 1 Scala source to \hello_sbt\sayisalLoto\target\scala-2.12\classes ...
[info] running SayisalTahmin
20,43,42,2,35,43

Test kosma
Haha ingilizce run kelimesinin bu contexte "kosmak" olarak cevrilmesine asiri uyuz oldugum icin bu basligi sectim. Neyse, simdilik test calistirmayi test etmek icin 3rd party bir testi framework (scala-test gibi) kullanmayacagiz. Hersey hardcore... Simdi src klasorunun icerisine su path uzerinden bir test dosyasi ekliyoruz: test/main/SayisalTahminSpec.scala

Guzel test edebilmek icin onceki SayisalTahmin.sclaa class'imizi cok az degistirmemeiz gerekiyor. Bu bile testingin ne kadar onemli bir klavuz oldugunu gosteriyor ama detaylara ilerde deginecegiz. 

object SayisalTahmin extends App {
  def tahminEt(): Seq[Int] = {
    (0 to 5).map(_ => scala.util.Random.nextInt(50))
  }

  override def main(args: Array[String]): Unit = {
    println(tahminEt().mkString(","))
  }
}

Daha sonra test class'i da ayni sekilde bir executable olacak simdilik:

object SayisalTahminSpec extends App {
  def runTests() : Unit = {
    assert(SayisalTahmin.tahminEt().size == 6, "6 tahmin icermeli")
  }

  override def main(args: Array[String]): Unit = {
    println("Testler calisiyor ...")
    runTests()
    println("Tum testler gecti.")
  }
}

Bu noktada test classini calistirmaya calisir isek, sayilsalTahmin/run komutu ile, src/main/scala 'daki implementasyon caliscaktir, test degil. Buna ek olarak test komutu da sadece bir 3rd party test framework integrasyonu varsa calisacaktir. Peki bu durumda nasil src/test/scala 'daki bir class'i nasil calistiracagiz? Bunun icin compile scope'undan test scope'una gecmemiz gerekiyor. 

Sbt birseyi calistirirken su uc degere gore calistirir:

project_axis/config_axis/task_key 

Biz bir onceki adimda sayisalLoto/run komutunu calistitiken aslinda config_axis degerini bos geciyoruz ve sbt default deger olarak Compile degerini aliyor. Hatta ilk bastaki project_axis degerini de bos gectigimizde (sadece run calistirinca) bu sefer de project_axis degerini de default degeri olarak mevcut aktif projeyi aliyor. Hatta detalari incelemek icin onceki postta inspect komutundan bahsetmistik. 

sbt:hello_sbt> inspect sayisalLoto/run
[info] Input task: Unit
[info] Description:
[info]  Runs a main class, passing along arguments provided on the command line.
[info] Provided by:
[info]  ProjectRef(uri("file:/hello_sbt/"), "sayisalLoto") / Compile / run

Burada goruldugu gibi default config Compile, dolayisi ile src/main/scala klasoru icerisinde run komutu calistiriliyor. 

Simdi yapmamiz gereken de bu config_axis degerini elle Test olarak set etmek ki, src/test/scala 'daki bir class uzerinde run calissin:

sayisalLoto/Test/run

Bu sefer test class'inin calistigini goruyoruz.

Yeni bir class
Simdi projemize yeni bir class ekliyoruz. Bu sefer verilen at listesi icerisinden birinci gelecek ati tahmin eden bir class gelistiriyoruz. SayisalLoto projesine bunu eklemek biraz sacma olacak ama idare edelim, ileride proje adini SansOyunlari seklinde degistiririz belki.

object AtYarisiTahmin extends App {
    def tahminEt(atlar: Array[String]): String = {
        atlar(scala.util.Random.nextInt(atlar.length)
    }

    override def main(args: Array[String]): Unit = {
        println(tahminEt(args))
    }
}

compile calistirdigimizda dikkat ederseniz sadece 1 dosyanin derlendigini goruyoruz. Bu da sbt'nin incremental compilation yapmasindan kaynaklaniyor. Yani sadece derlenmeye ihtiyaci olan dosyalar derleniyor. Class'i calistirirken at listesini de arguman olarak vermemiz gerekiyor:

sbt:hello_sbt> sayisalLoto/runMain AtYarisiTahmin "sahbatur" "baturalp" "baturmert"
[warn] multiple main classes detected: run 'show discoveredMainClasses' to see the list
[info] running AtYarisiTahmin evren uzay deniz
baturalp

Evet bu kosuda baturalp geldi. Burada runMain komutu ile verilen proje icerisinden verilen class'in main metodunu calistirmis olduk. 

Cok asiri ozet:
    - sub-project yapisi ve aggregator proje
    - sub-proje olusturma
    - scope'lar nelerdir (Test ve Compile)

Diger bir sbt postunda gorusmek uzere. 



Yorumlar

Bu blogdaki popüler yayınlar

Python'da Multithreading ve Multiprocessing

Threat Modeling 1

Encoding / Decoding