Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Homework7 #7

Merged
merged 21 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Homework7/Homework7.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MiniCrawler", "MiniCrawler\MiniCrawler.fsproj", "{7A2FFF58-1A54-4E4A-A5A7-C0C06E345050}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MiniCrawler.Tests", "MiniCrawler.Tests\MiniCrawler.Tests.fsproj", "{541551EE-AF15-4DCC-848D-36EDF6A07DD1}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Lazy", "Lazy\Lazy.fsproj", "{5310B6BA-3D02-4A8D-A067-A9452C046FC9}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Lazy.Tests", "Lazy.Tests\Lazy.Tests.fsproj", "{C21915B1-5326-48A3-B3B1-7C5E98736BCD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7A2FFF58-1A54-4E4A-A5A7-C0C06E345050}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A2FFF58-1A54-4E4A-A5A7-C0C06E345050}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A2FFF58-1A54-4E4A-A5A7-C0C06E345050}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A2FFF58-1A54-4E4A-A5A7-C0C06E345050}.Release|Any CPU.Build.0 = Release|Any CPU
{541551EE-AF15-4DCC-848D-36EDF6A07DD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{541551EE-AF15-4DCC-848D-36EDF6A07DD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{541551EE-AF15-4DCC-848D-36EDF6A07DD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{541551EE-AF15-4DCC-848D-36EDF6A07DD1}.Release|Any CPU.Build.0 = Release|Any CPU
{5310B6BA-3D02-4A8D-A067-A9452C046FC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5310B6BA-3D02-4A8D-A067-A9452C046FC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5310B6BA-3D02-4A8D-A067-A9452C046FC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5310B6BA-3D02-4A8D-A067-A9452C046FC9}.Release|Any CPU.Build.0 = Release|Any CPU
{C21915B1-5326-48A3-B3B1-7C5E98736BCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C21915B1-5326-48A3-B3B1-7C5E98736BCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C21915B1-5326-48A3-B3B1-7C5E98736BCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C21915B1-5326-48A3-B3B1-7C5E98736BCD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
24 changes: 24 additions & 0 deletions Homework7/Lazy.Tests/Lazy.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FsUnit" Version="6.0.0" />
<PackageReference Include="FsCheck" Version="2.16.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.0.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lazy\Lazy.fsproj" />
</ItemGroup>
</Project>
4 changes: 4 additions & 0 deletions Homework7/Lazy.Tests/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Program =

[<EntryPoint>]
let main _ = 0
67 changes: 67 additions & 0 deletions Homework7/Lazy.Tests/Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module Lazy.Tests

open Lazy
open NUnit.Framework
open FsUnit

let mutable counter = 0

let lazyConstructors =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

нужен ещё один тест, который внутри Supplier будет вызывать Interlocked.Increment &counter, чтобы проверить, что supplier у lock-реализации в многопоточной среде вызывается 1 раз

ну и с эксепшенами в многопоточной среде

[ (fun f -> SingleThreadedLazy f :> ILazy<obj>)
(fun f -> ThreadSafeLazy f :> ILazy<obj>)
(fun f -> LockFreeLazy f :> ILazy<obj>) ]
|> List.map (fun f -> TestCaseData(f))

let multiThreadLazyConstructors =
[ (fun f -> ThreadSafeLazy f :> ILazy<obj>)
(fun f -> LockFreeLazy f :> ILazy<obj>) ]
|> List.map (fun f -> TestCaseData(f))

[<TestCaseSource(nameof lazyConstructors)>]
let ``Value should compute only once`` (lazyConstructor: (unit -> obj) -> ILazy<obj>) =
let supplier () =
counter <- counter + 1
obj ()

counter <- 0

let lazyObject = lazyConstructor supplier
let firstCallValue = lazyObject.Get()
let secondCallValue = lazyObject.Get()

Assert.That(counter, Is.EqualTo(1))


[<TestCaseSource(nameof lazyConstructors)>]
let ``Computed value should be same for several Gets`` (lazyConstructor: (unit -> obj) -> ILazy<obj>) =
let supplier () = obj ()

let lazyObject = lazyConstructor supplier
let firstCallValue = lazyObject.Get()
let secondCallValue = lazyObject.Get()

Assert.That(firstCallValue, Is.EqualTo(secondCallValue))


[<TestCaseSource(nameof lazyConstructors)>]
let ``Exception in supplier should be thrown`` (lazyConstructor: (unit -> obj) -> ILazy<obj>) =
let lazyObject = lazyConstructor (fun () -> raise (System.Exception()))
Assert.Throws<System.Exception>(fun () -> lazyObject.Get() |> ignore) |> ignore


[<TestCaseSource(nameof multiThreadLazyConstructors)>]
let ``Multithread lazies test`` (lazyConstructor: (unit -> obj) -> ILazy<obj>) =
let supplier () = obj ()

let lazyObject = lazyConstructor supplier
let amountOfThreads = 8

let tasksArray =
Seq.init amountOfThreads (fun _ -> async { return lazyObject.Get() })

tasksArray
|> Async.Parallel
|> Async.RunSynchronously
|> Seq.distinct
|> Seq.length
|> should equal 1
12 changes: 12 additions & 0 deletions Homework7/Lazy/Lazy.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="Library.fs" />
</ItemGroup>

</Project>
66 changes: 66 additions & 0 deletions Homework7/Lazy/Library.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
module Lazy

type ILazy<'a> =
abstract member Get: unit -> 'a

type SingleThreadedLazy<'a>(supplier: unit -> 'a) =
let mutable value: Result<'a, exn> option = None

interface ILazy<'a> with
member this.Get() =
match value with
| Some(Ok v) -> v
| Some(Error ex) -> raise ex
| None ->
try
let v = supplier ()
value <- Some(Ok v)
v
with ex ->
value <- Some(Error ex)
raise ex

type ThreadSafeLazy<'a>(supplier: unit -> 'a) =
let mutable value: Result<'a, exn> option = None
let syncObj = obj ()

interface ILazy<'a> with
member this.Get() =
match value with
| Some(Ok v) -> v
| Some(Error ex) -> raise ex
| None ->
lock syncObj (fun () ->
match value with
| Some(Ok v) -> v
| Some(Error ex) -> raise ex
| None ->
try
let v = supplier ()
value <- Some(Ok v)
v
with ex ->
value <- Some(Error ex)
raise ex)

type LockFreeLazy<'a>(supplier: unit -> 'a) =
let mutable value: Result<'a, exn> option = None

interface ILazy<'a> with
member this.Get() =
match value with
| Some(Ok v) -> v
| Some(Error ex) -> raise ex
| None ->
try
let result = supplier ()

match System.Threading.Interlocked.CompareExchange(&value, Some(Ok result), None) with
| Some(Ok v) -> v
| Some(Error ex) -> raise ex
| None -> result
with ex ->
System.Threading.Interlocked.CompareExchange(&value, Some(Error ex), None)
|> ignore

raise ex

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

нужно точно так же паттерн-матчить, а не игнорировать результат с выбросом исключения, поскольку может произойти ситуация, когда успешный поток записал значение, а неуспешный не успел записать эксепшен -- получим два разных результата

24 changes: 24 additions & 0 deletions Homework7/MiniCrawler.Tests/MiniCrawler.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FsUnit" Version="6.0.0" />
<PackageReference Include="FsCheck" Version="2.16.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.0.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MiniCrawler\MiniCrawler.fsproj" />
</ItemGroup>
</Project>
4 changes: 4 additions & 0 deletions Homework7/MiniCrawler.Tests/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Program =

[<EntryPoint>]
let main _ = 0
16 changes: 16 additions & 0 deletions Homework7/MiniCrawler.Tests/Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module MiniCrawler.Tests

open MiniCrawler
open FsUnit
open NUnit.Framework

[<Test>]
Copy link

@DedSec256 DedSec256 May 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

нужны тесты, которые проверят, что в случае исключения при загрузке одной из страниц, для другой спешно выполнится подсчет размера

  let mock = Mock<HttClient>()

  mock.Setup(fun client -> client.GetStringAsync("hwproj.ru"))
      .ReturnsAsync("<html>нормальная ссылка / битая ссылка</html>");
          

downloadPages "hwproj.ru" mock.Object
 

https://github.com/devlooped/moq

let ``extractLinks test`` () =
let url = "https://example.com"

async {
let! htmlPage = downloadPageAsync url
let actualLinks = extractLinks htmlPage
let expectedLinks = [ "https://www.iana.org/domains/example" ]
Assert.AreEqual(expectedLinks, actualLinks)
}
46 changes: 46 additions & 0 deletions Homework7/MiniCrawler/Library.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module MiniCrawler

open System.Net.Http
open System.Text.RegularExpressions

let downloadPageAsync (url: string) =
async {
try
use client = new HttpClient()
let! html = client.GetStringAsync(url) |> Async.AwaitTask
return html
with ex ->
printfn "Failed to download %s: %s" url ex.Message
return ""
Copy link

@DedSec256 DedSec256 May 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

можно было вернуть None/Result

link, Some/None length

}

let extractLinks (html: string) =
let pattern = @"<a\s+(?:[^>]*?\s+)?href=([""'])(http[^""']+)\1"
let matches = Regex.Matches(html, pattern)
[ for m in matches -> m.Groups.[2].Value ]

let downloadPages (url: string) =
async {
let! mainPageHtml = downloadPageAsync url
let linksFromMainPage = extractLinks mainPageHtml

let downloadTasks =
linksFromMainPage
|> List.map (fun link ->
async {
let! html = downloadPageAsync link
return (link, html.Length)
})

let! results = Async.Parallel downloadTasks
return results |> List.ofArray
}

let printSizes (results: (string * int) list) =
results |> List.iter (fun (link, size) -> printfn "%s — %d" link size)

let downloadAndPrintSizes (url: string) =
async {
let! results = downloadPages url
printSizes results
}
12 changes: 12 additions & 0 deletions Homework7/MiniCrawler/MiniCrawler.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="Library.fs" />
</ItemGroup>

</Project>
Loading