Afhankelijkheden toevoegen

Deze tutorial beschrijft het gebruik van afhankelijkheden met fpm en hoe een fpm-projekt hergebruikt kan worden.

De standaardbibliotheek gebruiken

We beginnen een nieuw projekt met fpm: een programma via command-lineopties een bestand inleest, een of ander patroon zoekt en dan vervangt. Omdat we de functie om tekst te vervangen niet zelf willen schrijven gebruiken we de standaardbibliotheek (stdlib) als afhankelijkheid. In het manifestbestand definiëren we stdlib in de tabel dependencies:

fpm.toml
name = "demo"
version = "0.1.0"

[dependencies]
stdlib = "*"

Nu maken we een module met een procedure, die de substitutie uitvoert. Dit vergt drie stappen:

  1. een hele regel uit een bestand lezen

  2. een patroon in een string vervangen

  3. een nieuwe string naar het uitvoerbestand schrijven

We gebruiken hier de replace_all-functie uit de stdlib_strings module voor. De implementatie is hier weergegeven:

src/demo.f90
module demo
  use stdlib_io, only : getline
  use stdlib_strings, only : replace_all
  implicit none
  private

  public :: substitute

contains

  !> Read all lines from input, replace pattern and print it to output
  subroutine substitute(input, output, pattern, replacement)
    !> Formatted input unit
    integer, intent(in) :: input
    !> Formatted output unit
    integer, intent(in) :: output
    !> Pattern to replace in input
    character(len=*), intent(in) :: pattern
    !> Replacement for pattern in output
    character(len=*), intent(in) :: replacement

    character(len=:), allocatable :: line
    integer :: stat

    do
      call getline(input, line, stat)
      if (stat /= 0) exit
      write(output, '(a)') replace_all(line, pattern, replacement)
    end do
  end subroutine substitute

end module demo

Tenslotte hebben we een stuurprogramma nodig om onze nieuwe functie te gebruiken.

app/main.f90
program main
  use, intrinsic :: iso_fortran_env, only : output_unit
  use demo, only : substitute
  implicit none
  character(len=256) :: pattern, replacement, input_file
  integer :: input

  call get_command_argument(1, pattern)
  call get_command_argument(2, replacement)
  call get_command_argument(3, input_file)

  open(newunit=input, file=input_file, status='old')
  call substitute(input, output_unit, trim(pattern), trim(replacement))
  close(input)
end program main

We kunnen ons programma via fpm testen:

❯ fpm run -- demo substitute fpm.toml
name = "substitute"
version = "0.1.0"

[dependencies]
stdlib = "*"

Een test-framework toevoegen

Before we continue implementing new features, we want to add some tests to verify that our implementation keeps working as we modify it. A minimalist testing framework is available with test-drive. Since the testing framework is only required when developing the package itself, but not for other packages which might in the future make use of our modules, we add it as a development dependency. The test-drive package is added in the dev-dependencies table as shown below

fpm.toml
name = "demo"
version = "0.1.0"

[dependencies]
stdlib = "*"

[dev-dependencies]
test-drive.git = "https://github.com/fortran-lang/test-drive"
test-drive.tag = "v0.4.0"

Notitie

Voor een afhankelijkheid die voor de ontwikkeling van een nieuw pakket nodig is zoals het test-framework, willen we liefst een specifieke versie hebben. Hiervoor geven we de juiste tag aan.

We schrijven vervolgens een simpele test. Omdat onze functie met unitnummers werkt, maken we tijdelijke files aan voor de invoer en om de uitvoer op te vangen. Voor het moment is die test een substitutie in een enkele regel.

test/main.f90
module test_demo
  use demo, only : substitute
  use stdlib_io, only : getline
  use testdrive, only : error_type, unittest_type, new_unittest, check
  implicit none
  private

  public :: collect_demo

contains

  !> Collect all exported unit tests
  subroutine collect_demo(testsuite)
    !> Collection of tests
    type(unittest_type), allocatable, intent(out) :: testsuite(:)

    testsuite = [new_unittest("substitute", test_substitute)]
  end subroutine collect_demo

  !> Check substitution of a single line
  subroutine test_substitute(error)
    !> Error handling
    type(error_type), allocatable, intent(out) :: error
    integer :: input, output, stat
    character(len=:), allocatable :: line
    open(newunit=input, status="scratch")
    write(input, '(a)') "This is a valid test"
    rewind(input)

    open(newunit=output, status="scratch")
    call substitute(input, output, "test", "example")
    close(input)

    rewind(output)
    call getline(output, line, stat)
    close(output)

    call check(error, line, "This is a valid example")
  end subroutine test_substitute
end module test_demo

program tester
  use, intrinsic :: iso_fortran_env, only : error_unit
  use testdrive, only : run_testsuite
  use test_demo, only : collect_demo
  implicit none
  integer :: stat

  stat = 0
  call run_testsuite(collect_demo, error_unit, stat)

  if (stat > 0) then
    write(error_unit, '(i0, 1x, a)') stat, "test(s) failed!"
    error stop
  end if

end program tester

We draaien onze nieuwe test met fpm

❯ fpm test
  Starting substitute ... (1/1)
       ... substitute [PASSED]

Het schrijven van dit soort tijdelijke bestanden is vaak een herhaling van zetten en daarom doen we dit in een aparte procedure die we meerdere keren kunnen gebruiken.

Specifieke afhankelijkheden per doel (target)

Afhankelijkheden kunnen ook gebruikt voor specifieke doelen. Een simpel voorbeeld is een pakket voor het verwerken van opdrachtopties dat alleen door een programma wordt gebruikt, maar niet door de bibliotheken die voor dat programma nodig zijn.

fpm.toml
name = "demo"
version = "0.1.0"

[dependencies]
stdlib = "*"

[dev-dependencies]
test-drive.git = "https://github.com/fortran-lang/test-drive"
test-drive.tag = "v0.4.0"

[[executable]]
name = "demo"
[executable.dependencies]
M_CLI2.git = "https://github.com/urbanjost/M_CLI2"

We zetten de structuur van ons hoofdprogram iets anders op, om [M_CLI2] te kunnen gebruiken voor het afhandeling van de opdrachtregels. De array unnamed bevat alle positie-afhankelijke argumenten. We gebruiken de eerste twee als patroon en vervanging en alle verdere als invoer. Tenslotte voeren we een optie in om de uitvoer om te leiden. Het resultaat ziet er zo uit:

app/main.f90
program main
  use, intrinsic :: iso_fortran_env, only : output_unit
  use demo, only : substitute
  use m_cli2, only : set_args, unnamed, sget
  implicit none
  character(len=:), allocatable :: input_file, output_file, pattern, replacement
  integer :: input, output, i

  call set_args("--output:o ''")

  output_file = trim(sget("output"))
  if (len(output_file) > 0) then
    open(file=output_file, newunit=output)
  else
    output = output_unit
  end if

  pattern = trim(unnamed(1))
  replacement = trim(unnamed(2))

  do i = 3, size(unnamed)
    input_file = trim(unnamed(i))
    open(file=input_file, newunit=input, status='old')
    call substitute(input, output_unit, trim(pattern), trim(replacement))
    close(input)
  end do

  if (output /= output_unit) close(output)
end program main

We voeren opnieuw een kleine test met fpm uit

❯ fpm run -- demo substitute fpm.toml
name = "substitute"
version = "0.1.0"

[dependencies]
stdlib = "*"

[dev-dependencies]
test-drive.git = "https://github.com/fortran-lang/test-drive"
test-drive.tag = "v0.4.0"

[[executable]]
name = "substitute"
[executable.dependencies]
M_CLI2.git = "https://github.com/urbanjost/M_CLI2"

De uitvoer ziet eruit als verwacht, met twee vervangingen.

Samenvatting

In deze tutorial hebben we geleerd hoe je

  • een afhankelijkheid van een fpm project in het manifestbestand specificeert

  • afhankelijkheden toevoegt voor testen

  • afhankelijkheden specificeert voor het programma zelf